// dOOdad - Object-oriented programming framework for Javascript
// File: Tools.js - Useful tools
// Project home: https://sourceforge.net/projects/doodad-js/
// Author: Claude Petit, Quebec city
// Copyright @ 2015

if (!window.Doodad) {
	Doodad = {
		parent: window,
	};
};

if (!Doodad.Tools) {
	Doodad.Tools = {
		parent: Doodad,
		
		enableNatives: true, // "false" is for test and debug purposes only
		enableSetPrototypeOf: false, // When "true", Firefox gives a warning about performance issues, telling to create the object with the correct prototype. So what is the the purpose of this function ?
		defaultScriptTimeout: 3000, // milliseconds
		
		// Events
		onscriptinit: null,
		onscriptloading: null,
		onscriptload: null,
		onscripterror: null,
	};
};

Doodad.Tools.init = function() {
	"use strict";

	var tools = this;
	
	if (tools.initialized) {
		return;
	};
	

	var __functionToString__ = Function.prototype.toString;
	tools.isFunction = function isFunction(obj) {
		return (typeof obj === 'function');
	};
	tools.isNativeFunction = function isNativeFunction(obj) {
		if (tools.isFunction(obj)) {
			var str = __functionToString__.call(obj),
				index1 = str.indexOf('{') + 1,
				index2 = str.indexOf('[native code]', index1);
			if (index2 < 0) {
				return false;
			};
			str = str.substring(index1, index2);
			for (var i = 0; i < str.length; i++) {
				var chr = str[i];
				if ((chr !== '\n') && (chr !== '\r') && (chr !== '\t') && (chr !== ' ')) {
					return false;
				};
			};
			return true;
		} else {
			return false;
		};
	};
	tools.isCustomFunction = function isCustomFunction(obj) {
		if (tools.isFunction(obj)) {
			var str = __functionToString__.call(obj),
				index1 = str.indexOf('{') + 1,
				index2 = str.indexOf('[native code]', index1);
			if (index2 < 0) {
				return true;
			};
			str = str.substring(index1, index2);
			for (var i = 0; i < str.length; i++) {
				var chr = str[i];
				if ((chr !== '\n') && (chr !== '\r') && (chr !== '\t') && (chr !== ' ')) {
					return true;
				};
			};
			return false;
		} else {
			return false;
		};
	};


	var doodad = tools.parent,
	
		// Prototype functions
		__objectToString__ = Object.prototype.toString,
		__objectHasOwnProperty__ = Object.prototype.hasOwnProperty,
		__stringIndexOf__ = String.prototype.indexOf,
		__stringLastIndexOf__ = String.prototype.lastIndexOf,
		__arrayConstructor__ = Array.prototype.constructor,
		__arraySlice__ = Array.prototype.slice,
		__arraySplice__ = Array.prototype.splice,
		__arrayPush__ = Array.prototype.push,
		__arrayUnshift__ = Array.prototype.unshift,

		// Polyfills

		// "Object.keys" Polyfill from Mozilla Developer Network.
		// NOTE: "hasDontEnumBug" is "true" when a property in "defaultNonEnumerables" is still non-enumerable while being changed to an own property.
		__hasDontEnumBug__ = !({ toString: null }).propertyIsEnumerable('toString'),
		__defaultNonEnumerables__ = [
		  'toString',
		  'toLocaleString',
		  'valueOf',
		  'hasOwnProperty',
		  'isPrototypeOf',
		  'propertyIsEnumerable',
		  'constructor',
		  'length',
		],
		__defaultNonEnumerablesLen__ = __defaultNonEnumerables__.length,
		__supportsNewOnEvent__ = (function() {
			if (!tools.isFunction(window.Event)) {
				return false;
			};
			try {
				// Throws "Illegal constructor" on some browsers.
				new Event('click');
			} catch(ex) {
				return false;
			};
			return true;
		})(),
		__functionBind__ = (tools.enableNatives && tools.isNativeFunction(Function.prototype.bind) ? Function.prototype.bind : undefined),
		__objectKeys__ = (tools.enableNatives && tools.isNativeFunction(Object.keys) ? Object.keys : undefined),
		__objectDefineProperty__ = (tools.enableNatives && tools.isNativeFunction(Object.defineProperty) ? Object.defineProperty : undefined),
		__objectCreate__ = (tools.enableNatives && tools.isNativeFunction(Object.create) ? Object.create : undefined),
		__objectGetPrototypeOf__ = (tools.enableNatives && tools.isNativeFunction(Object.getPrototypeOf) ? Object.getPrototypeOf : undefined),
		__objectSetPrototypeOf__ = (tools.enableNatives && tools.isNativeFunction(Object.setPrototypeOf) ? Object.setPrototypeOf : undefined),
		__objectGetOwnPropertyNames__ = (tools.enableNatives && tools.isNativeFunction(Object.getOwnPropertyNames) ? Object.getOwnPropertyNames : undefined),
		__objectAssign__ = (tools.enableNatives && tools.isNativeFunction(Object.assign) ? Object.assign : undefined),
		__arrayIsArray__ = (tools.enableNatives && tools.isNativeFunction(Array.isArray) ? Array.isArray : undefined),
		__arrayIndexOf__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.indexOf) ? Array.prototype.indexOf : undefined),
		__arrayLastIndexOf__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.lastIndexOf) ? Array.prototype.lastIndexOf : undefined),
		__arrayForEach__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.forEach) ? Array.prototype.forEach : undefined),
		__arrayMap__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.map) ? Array.prototype.map : undefined),
		__arrayFilter__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.filter) ? Array.prototype.filter : undefined),
		__arrayEvery__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.every) ? Array.prototype.every : undefined),
		__arraySome__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.some) ? Array.prototype.some : undefined),
		__arrayReduce__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.reduce) ? Array.prototype.reduce : undefined),
		__arrayReduceRight__ = (tools.enableNatives && tools.isNativeFunction(Array.prototype.reduceRight) ? Array.prototype.reduceRight : undefined),
		__windowCustomEvent__ = (tools.enableNatives && tools.isFunction(window.CustomEvent) ? window.CustomEvent : undefined);
		
		
	tools.isNothing = function isNothing(obj) {
		return (obj === null) || (obj === undefined);
	};
	tools.isNumber = function isNumber(obj) {
		// typeof 1 ---> 'number'
		// typeof Object(1) ---> 'object'
		// 1 === +1 ---> true
		// 1 == +1 ---> true
		// Object(1) === +Object(1) ---> false
		// Object(1) == +Object(1) ---> true
		return (obj == +obj);
	};
	tools.isInteger = function isInteger(obj) {
		// Source: Stack Overflow
		return (obj == +obj) && (obj === (obj | 0));
	};
	tools.isFloat = function isFloat(obj) {
		// Source: Stack Overflow
		return (obj == +obj) && (obj !== (obj | 0));
	};
	tools.isString = function isString(obj) {
		return (typeof obj === 'string');
	};
	tools.isDate = function isDate(obj) {
		return (__objectToString__.call(obj) === '[object Date]');
	};
	tools.isArray = (__arrayIsArray__ || (function isArray(obj) {
		return (__objectToString__.call(obj) === '[object Array]');
	}));
	tools.isArrayAndNotEmpty = function isArrayAndNotEmpty(obj) {
		if (tools.isArray(obj)) {
			for (var key = 0; key < obj.length; key++) {
				if (key in obj) {
					return true;
				};
			};
		};
		return false;
	};
	tools.isArrayLike = function isArrayLike(obj) {
		//     Unbelievable : There is no final ways to detect an array-like object !!!!
		if (tools.isNothing(obj)) {
			return false;
		};
		var toString = __objectToString__.call(obj);
		if ((toString === '[object Object]') || (toString === '[object Function]')) {
			return false;
		};
		obj = Object(obj).length;
		return (obj === +obj) && (obj === (obj | 0)) && (obj >= 0);
	};
	tools.isArrayLikeAndNotEmpty = function isArrayLikeAndNotEmpty(obj) {
		if (tools.isArrayLike(obj)) {
			obj = Object(obj);
			for (var key = 0; key < obj.length; key++) {
				if (key in obj) {
					return true;
				};
			};
		};
		return false;
	};
	tools.isObject = function isObject(obj) {
		return (__objectToString__.call(obj) === '[object Object]');
	};
	tools.isObjectLike = function isObjectLike(obj) {
		if ((obj === undefined) || (obj === null)) {
			return false;
		};
		var toString = __objectToString__.call(obj);
		return ((toString === '[object Object]') || (toString === '[object Function]'))
	};
	tools.isError = function isError(obj) {
		// NOTE: Very strange... The "[object Error]" string is no longer returned.
		return ((obj instanceof Error) || (__objectToString__.call(obj) === '[object Error]'));
	};
	tools.isElement = function isElement(obj) {
		// Source: Stack Overflow
		return ((typeof HTMLElement === "function")	? (obj instanceof HTMLElement) : obj && (typeof obj === "object") && (obj.nodeType === 1) && (typeof obj.nodeName === "string"));
	};
	tools.isNothingOrEmpty = function isNothingOrEmpty(obj) {
		return tools.isNothing(obj) || (tools.isArrayLike(obj) && !obj.length);
	};
	tools.isEmpty = function isEmpty(obj) {
		return tools.isArrayLike(obj) && !obj.length;
	};
	tools.isStringAndNotEmpty = function isStringAndNotEmpty(obj) {
		return tools.isString(obj) && !!obj.length;
	};
	tools.isStringAndNotEmptyTrim = function isStringAndNotEmptyTrim(obj) {
		return tools.isString(obj) && !!obj.trim().length;
	};
	tools.isObjectAndNotEmpty = function isObjectAndNotEmpty(obj) {
		// "Object.keys" Polyfill from Mozilla Developer Network.
		if (tools.isObject(obj)) {
			var key, i;
			for (key in obj) {
				if (__objectHasOwnProperty__.call(obj, key)) {
					return true;
				};
			};
			if (__hasDontEnumBug__) {
				for (i = 0; i < __defaultNonEnumerablesLen__; i++) {
					key = __defaultNonEnumerables__[i];
					if (__objectHasOwnProperty__.call(obj, key)) {
						return true;
					};
				};
			};
		};
		return false;
	};
	tools.isObjectLikeAndNotEmpty = function isObjectLikeAndNotEmpty(obj) {
		if (tools.isObjectLike(obj)) {
			obj = Object(obj);
			var key, i;
			if (tools.isArrayLike(obj)) {
				var len = obj.length;
				for (key in obj) {
					if ((isNaN(key) || !isFinite(key)) && __objectHasOwnProperty__.call(obj, key)) {
						return true;
					};
				};
			} else {
				// "Object.keys" Polyfill from Mozilla Developer Network.
				for (key in obj) {
					if (__objectHasOwnProperty__.call(obj, key)) {
						return true;
					};
				};
			};
			if (__hasDontEnumBug__) {
				for (i = 0; i < __defaultNonEnumerablesLen__; i++) {
					key = __defaultNonEnumerables__[i];
					if (__objectHasOwnProperty__.call(obj, key)) {
						return true;
					};
				};
			};
		};
		return false;
	};

	
	tools.format = function format(message, params) {
		// WARNING: Don't use "doodad.ASSERT" inside this function !!!
		if (!tools.isString(message) || (!tools.isArray(params) && !tools.isObject(params))) {
			// Invalid parameters. DO NOT CALL doodad.ASSERT
			return '';
		};
		var result = '',
			pos,
			lastPos = 0,
			isKey = false;
		while ((pos = message.indexOf('~', lastPos)) >= 0) {
			var key = message.substring(lastPos, pos);
			if (isKey) {
				if (key.length) {
					result += params[key];
				} else {
					result += '~';
				};
				isKey = false;
			} else {
				result += key;
				isKey = true;
			};
			lastPos = pos + 1;
		};
		return isKey ? result : result + message.substring(lastPos);
	};
	
	
	var __ASSERT__ = function ASSERT(expr, /*optional*/message, /*optional*/params) {
		// IMPORTANT: You must use it like this : 
		//          Doodad.ASSERT && Doodad.ASSERT(expr, message, params);
		
		// WARNING: Don't use "doodad.ASSERT" inside this function !!!
		if (!expr) {
			if (tools.isStringAndNotEmptyTrim(message)) {
				message = "Assertion failed: " + tools.format(message, params || []);
			} else {
				message = "Assertion failed.";
			};
			debugger;
			throw new Error(message);
		};
	};
	tools.enableAsserts = function enableAsserts() {
		doodad.ASSERT = __ASSERT__;
	};
	tools.disableAsserts = function disableAsserts() {
		delete doodad.ASSERT;
	};
	tools.disableAsserts();

	
	tools.LogLevels = {
		Debug: 0,
		Info: 1,
		Warning: 2,
		Error: 3,
	};
	var __logLevelsName__ = [
		'debug',
		'info',
		'warning',
		'error',
	];
	tools.logLevel = tools.LogLevels.Error;
	tools.log = function log(level, message, params) {
		if (doodad.ASSERT) {
			doodad.ASSERT(tools.isInteger(level), "Invalid log level.");
			doodad.ASSERT(!tools.isEmpty(message), "Invalid message.");
		};
		
		if (window.console && (level >= tools.logLevel)) {
			if (params) {
				message = tools.format(message + '', params);
			};
			if (tools.isString(message)) {
				message = __logLevelsName__[level] + ': ' + message;
			};
			if ((level === tools.LogLevels.Info) && console.info) {
				console.info(message);
			} else if ((level === tools.LogLevels.Warning) && console.warn) {
				console.warn(message);
			} else if ((level === tools.LogLevels.Error) && console.error) {
				console.error(message);
			} else if ((level === tools.LogLevels.Error) && console.exception) {
				console.exception(message);
			} else if (console.log) {
				console.log(message);
			};
		};
	};

	
	
	tools.box = function box(value) {
		// <PRB> "Object(null)" doesn't store "null".
		if (this.constructor === tools.box) {
			tools.defineProperty(this, '__BOXED_VALUE__', {
				value: value,
			});
			return this;
		};
		return ((value instanceof tools.box) ? value : (new tools.box(value)));
	};
	tools.unbox = function unbox(value) {
		// <PRB> "Object(null)" doesn't store "null".
		return ((value instanceof tools.box) ? value.__BOXED_VALUE__ : value);
	};
	tools.bind = function bind(fn, obj) {
		if (tools.isNothing(fn)) {
			return fn;
		};
		doodad.ASSERT && doodad.ASSERT(tools.isFunction(fn), "Invalid function.");
		fn = tools.unbind(fn);
		var newFn;
		if (__functionBind__) {
			newFn = __functionBind__.call(fn, obj);
		} else {
			newFn = function(/*paramarray*/) {
				return fn.apply(obj, arguments);
			};
		};
		tools.defineProperty(newFn, '__ORIGINAL_FUNCTION__', {
			value: fn,
		});
		return newFn;
	};
	tools.unbind = function unbind(fn) {
		if (tools.isNothing(fn)) {
			return fn;
		};
		doodad.ASSERT && doodad.ASSERT(tools.isFunction(fn), "Invalid function.");
		return (fn.__ORIGINAL_FUNCTION__ || fn);
	};
	
	tools.defineProperty = (__objectDefineProperty__ || (function defineProperty(obj, name, descriptor) {
		if (!tools.isObjectLike(descriptor)) {
			throw new TypeError("Invalid descriptor.");
		};
		if (tools.hasKey(descriptor, 'get') || tools.hasKey(descriptor, 'set')) {
			throw new TypeError("Properties are not supported.");
		} else {
			if (!tools.isObjectLike(obj)) {
				throw new TypeError("Not an object.");
			};
			obj[name] = descriptor.value;
		};
	}));
	
	var __hasProto__ = !!({}.__proto__);
	tools.createObject = (__objectCreate__ || (function() {
		// Enhanced polyfill taken from Mozilla Developer Network. 
		if (__hasProto__) {
			var tmp = function Object() {};
			return (function createObject(proto, /*optional*/properties) {
				doodad.ASSERT && doodad.ASSERT(tools.isObjectLike(proto), "Invalid prototype.");
				
				tmp.prototype = proto;
				var obj = new tmp();
				tmp.prototype = null; // free memory

				// Reproduce behavior of "Object.create".
				obj.constructor = Object;
				
				var keys = tools.keys(properties),
					keysLen = keys.length,
					i,
					name;
					
				for (i = 0; i < keysLen; i++) {
					name = keys[i];
					tools.defineProperty(obj, name, properties[name]);
				};
				
				return obj;
			});
		} else {
			return (function createObject(proto, /*optional*/properties) {
				doodad.ASSERT && doodad.ASSERT(tools.isObjectLike(proto), "Invalid prototype.");
				
				var tmp = function Object() {};
				tmp.prototype = proto;
				var obj = new tmp();

				// Removed for "tools.getPrototypeOf".
				//// Reproduce behavior of "Object.create".
				//obj.constructor = Object;
				
				var keys = tools.keys(properties),
					keysLen = keys.length,
					i,
					name;
					
				for (i = 0; i < keysLen; i++) {
					name = keys[i];
					tools.defineProperty(obj, name, properties[name]);
				};
				
				return obj;
			});
		};
	})());

	tools.getPrototypeOf = (__objectGetPrototypeOf__ || (function getPrototypeOf(obj) {
		if (__hasProto__) {
			// For browsers implementing "__proto__".
			return function getPrototypeOf(obj) {
				if (tools.isNothing(obj)) {
					throw new TypeError("Can't convert to object.");
				};
				return Object(obj).__proto__;
			};
		} else {
			// For very old browsers.
			// TODO: Test with old browsers.
			return function getPrototypeOf(obj) {
				if (tools.isNothing(obj)) {
					throw new TypeError("Can't convert to object.");
				};
				return Object(obj).constructor.prototype;
			};
		};
	})());
	
	tools.setPrototypeOf = (tools.enableSetPrototypeOf && __objectSetPrototypeOf__ || (function() {
		if (tools.enableSetPrototypeOf && (__hasProto__)) {
			// For browsers implementing "__proto__".
			return function setPrototypeOf(obj, proto) {
				if (tools.isNothing(obj)) {
					return obj;
				};
				if (!tools.isObjectLike(obj)) {
					throw new TypeError("Not an object.");
				};
				if (tools.isObjectLike(proto)) {
					obj.__proto__ = proto;
				};
				return obj; 
			};
		} else {
			// For very old browsers or when "setPrototypeOf" is disabled.
			// TODO: Test with old browsers.
			return function setPrototypeOf(obj, proto) {
				if (!tools.isObjectLike(obj)) {
					throw new TypeError("Not an object.");
				};
				var tmp = tools.createObject(proto, {
					constructor: {
						value: obj.constructor,
						writable: true,
					},
				});
				tools.extend(tmp, obj);
				return tmp;
			};
		};
	})());
	
	
	
	
	var __escapeHtmlElement__ = document.createElement('div');
	tools.escapeHtml = function(text) {
		__escapeHtmlElement__.textContent = text || '';
		return __escapeHtmlElement__.innerHTML;
	};
	
	
	tools.relativeToAbsolutePath = function relativeToAbsolutePath(/*optional*/basePath, path) {
		if (doodad.ASSERT) {
			doodad.ASSERT(tools.isNothing(basePath) || tools.isString(basePath), "Invalid base path.");
			doodad.ASSERT(tools.isString(path), "Invalid path.");
		};

		var overflow = false;

		if (basePath) {
			if (basePath.length && (basePath[0] === '/')) {
				basePath = basePath.substr(1);
			};
			if (basePath.length && (basePath[basePath.length - 1] === '/')) {
				basePath = basePath.substr(0, basePath.length - 1);
			};
			basePath = basePath.split('/');
			var pos = 0;
			while (pos < basePath.length) {
				var name = basePath[pos].trim(),
					count = 0;
				if (name === '.') {
					count = 1;
				} else if (name === '..') {
					count = 2;
				};
				if (count) {
					var i = pos - count + 1;
					if (i < 0) {
						overflow = true;
						break;
					};
					basePath.splice(i, count);
					pos -= (count - 1);
				} else {
					pos++;
				};
			};
		};
	
		if (path) {
			if (path.length && (path[0] === '/')) {
				path = path.substr(1);
				basePath = null;
			}
			if (path.length && (path[path.length - 1] === '/')) {
				path = path.substr(0, path.length - 1);
			};
			path = path.split('/');
			var pos = 0;
			while (pos < path.length) {
				var name = path[pos].trim(),
					count = 0;
				if (name === '.') {
					count = 1;
				} else if (name === '..') {
					count = 2;
				};
				if (count) {
					var i = pos - count + 1;
					if (i < 0) {
						if (basePath && (((basePath.length - 1) > 0) || (basePath[0].length))) {
							basePath.splice(basePath.length - 1, 1);
							i = 0;
							count = 1;
						} else {
							overflow = true;
							break;
						};
					};
					path.splice(i, count);
					pos -= (count - 1);
				} else {
					pos++;
				};
			};
			path = path.join('/');
		};
		
		if (basePath) {
			basePath = basePath.join('/');
		};

		return {
			basePath: basePath,
			path: path,
			overflow: overflow,
		};
	};
	
	tools.url = function url(/*optional*/protocol, /*optional*/user, /*optional*/password, /*optional*/domain, /*optional*/port, /*optional*/path, /*optional*/file, /*optional*/args, /*optional*/anchor, /*optional*/isAbsolute, /*optional*/isEscaped, /*optional*/isWindows) {
		if (this instanceof tools.url) {
			this.protocol = protocol;
			this.user = user;
			this.password = password;
			this.domain = domain;
			this.port = port;
			this.path = path;
			this.file = file;
			this.args = args;
			this.anchor = anchor;
			this.isAbsolute = !!isAbsolute;
			this.isEscaped = !!isEscaped;
			this.isWindows = !!isWindows;
			return this;
		} else {
			return null;
		};
	};
	
	tools.url.prototype.toString = function toString() {
		doodad.ASSERT && doodad.ASSERT(tools.isNothing(this) || (this instanceof tools.url), "Invalid url object.");

		var result = '';
		
		if (this) {
			var protocol = this.protocol;
			if (protocol) {
				protocol = protocol.toLowerCase();
			};
			if (!tools.isNothing(protocol)) {
				if (protocol.substr(protocol.length - 3) !== '://') {
					protocol += '://';
				};
			};
			var path = this.path;
			if (!tools.isNothing(protocol)) {
				result += protocol;
			};
			var val;
			if (protocol === 'file://') {
				result += '/';
			} else {
				if (!tools.isNothing(this.domain)) {
					val = this.user;
					if (!tools.isNothing(val)) {
						if (!this.isEscaped) {
							val = escape(val);
						};
						result += val;
						val = this.password;
						if (!tools.isNothing(val)) {
							if (!this.isEscaped) {
								val = escape(val);
							};
							result += ':' + val;
						};
						result += '@';
					};
					result += this.domain.toLowerCase();
					if (!tools.isNothing(this.port)) {
						result += ':' + this.port;
					};
					if (!tools.isNothing(this.path) || !tools.isNothing(this.file)) {
						result += '/';
					};
				};
			};
			var path = this.path,
				file = this.file;
			if (!tools.isNothing(file)) {
				if (file.length && (file[0] === '/')) {
					var pos = file.lastIndexOf('/');
					path = file.substring(0, pos + 1);
					file = file.substring(pos + 1);
				} else if (file.length && (file[file.length - 1] === '/')) {
					if (path && path.length && (path[path.length - 1] === '/')) {
						path = path.substr(0, path.length - 1);
					};
					path = (path || '') + '/' + file;
					file = null;
				};
			};
			if (!tools.isNothing(path)) {
				if (this.isAbsolute) {
					if (path.length && (path[0] === '/')) {
						path = path.substr(1);
					};
					if (path.length && (path[path.length - 1] === '/')) {
						path = path.substr(0, path.length - 1);
					};
				} else {
					path = tools.relativeToAbsolutePath(null, path).path;
				};
			};
			if (!tools.isNothing(file)) {
				if (!this.isAbsolute) {
					file = tools.relativeToAbsolutePath(path, file);
					path = file.basePath;
					file = file.path;
				};
			};
			if (!tools.isNothing(path)) {
				if (!this.isEscaped) {
					if (this.isWindows) {
						// Don't escape drive letter ("F:")
						var drive,
							pos1 = path.indexOf('/'),
							pos2 = path.indexOf(':');
						if ((pos2 >= 0) && ((pos1 < 0) || (pos2 < pos1))) {
							path = path.substring(0, pos2 + 1) + escape(path.substring(pos2 + 1));
						} else {
							path = escape(path);
						};
					} else {
						path = escape(path);
					};
				};
				result += path;
				if ((path.length > 1) && (path[path.length - 1] !== '/')) {
					result += '/';
				};
			};
			if (!tools.isNothing(file)) {
				if (!this.isEscaped) {
					file = escape(file);
				};
				result += file;
			};
			if (tools.isArray(this.args)) {
				result += '?';
				if (this.args.length) {
					for (var i = 0; i < this.args.length; i++) {
						var arg = this.args[i];
						val = arg.name;
						if (!tools.isNothing(val)) {
							if (!this.isEscaped) {
								val = escape(val);
							};
							result += val;
						};
						val = arg.value;
						if (!tools.isNothing(val)) {
							if (!this.isEscaped) {
								val = escape(val);
							};
							result += '=' + val;
						};
						result += '&';
					};
					result = result.substr(0, result.length - 1);
				};
			};
			if (!tools.isNothing(this.anchor)) {
				val = this.anchor;
				if (!this.isEscaped) {
					val = escape(val);
				};
				result += '#' + val;
			};
		};
		
		return result;
	};

	//tools.parseUrlRegEx = /^([a-zA-Z]+[:][/][/])?([^@]*[@])?([^/?:]+)([:][^/?]*)?([/][^?]*[/]?)?([?].*)?/
	tools.parseUrlRegEx = /([^@]*[@])?([^:/?#]+)?([:][^/?#]*)?([/][^?#]*[/]?)?([?][^#]*)?([#].*)?/
	tools.parseUrl = function parseUrl(url, /*optional*/removeEscapes, /*optional*/isAbsolute) {
		// a=tools.parseUrl("http://user:password@www.mydomain.com:8080/Doodad/v1/doodad.js?v=1&a=&b&#anchor")
		// a.toString()
		// a=tools.parseUrl("http://user:password@www.mydomain.com:8080/Doodad/v1/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com:8080/Doodad/v1/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com/Doodad/v1/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com/doodad.js")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com/")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com")
		// a.toString()
		// a=tools.parseUrl("http://")
		// a.toString()
		// a=tools.parseUrl("")  // should be null
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com/")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com/Doodad/v1/")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com/Doodad/v1/doodad.js")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com/Doodad/v1/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com/doodad.js")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("www.mydomain.com/?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com/Doodad%20Folder/doodad%20script.js?the%20name=the%20value#the%20anchor")
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com/Doodad%20Folder/doodad%20script.js?the%20name=the%20value#the%20anchor", true)
		// a.toString()
		// a=tools.parseUrl("file:///F:/Doodad/v1/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("file://F:/Doodad/v1/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("file:///F:/Doodad%20Folder/doodad%20script.js?the%20name=the%20value#the%20anchor")
		// a.toString()
		// a=tools.parseUrl("file:///F:/Doodad%20Folder/doodad%20script.js?the%20name=the%20value#the%20anchor", true)
		// a.toString()
		// a=tools.parseUrl("file:///home/doodad/v1/doodad.js?v=1&a=&b&")
		// a.toString()
		// a=tools.parseUrl("file:///home/Doodad%20User/doodad%20script.js?the%20name=the%20value#the%20anchor")
		// a.toString()
		// a=tools.parseUrl("file:///home/Doodad%20User/doodad%20script.js?the%20name=the%20value#the%20anchor", true)
		// a.toString()
		// a=tools.parseUrl("http://www.mydomain.com/Doodad/v1/./doodad.js")
		// a.toString()        // "http://www.mydomain.com/Doodad/v1/doodad.js"
		// a=tools.parseUrl("http://www.mydomain.com/Doodad/v1/../doodad.js")
		// a.toString()        // "http://www.mydomain.com/Doodad/doodad.js"
		// a=tools.parseUrl("http://www.mydomain.com/Doodad/v1/../../doodad.js")
		// a.toString()        // "http://www.mydomain.com/doodad.js"
		// a=tools.parseUrl("http://www.mydomain.com/Doodad/v1/../../../doodad.js")
		// a.toString()        // "http://www.mydomain.com/../doodad.js"
		// a=tools.parseUrl("http://www.mydomain.com/Doodad/v1/doodad.js")
		// a.file = '../doodad.js'
		// a.toString()       // "http://www.mydomain.com/Doodad/doodad.js"
		// a.file = '/doodad.js'
		// a.toString()        // "http://www.mydomain.com/doodad.js"
		// a.file = '/Doodad/'
		// a.toString()        // "http://www.mydomain.com/Doodad/"

		doodad.ASSERT && doodad.ASSERT(tools.isString(url), "Invalid url string.");

		// <PRB> Why does group "([a-zA-Z]+[:][/][/])?" is moved with url "file:///home/doodad/v1/doodad.js" ?
		var protocol = null,
			pos = url.indexOf("://");
		if (pos >= 0) {
			protocol = url.substr(0, pos + 3);
			url = url.substr(pos + 3);
		};

		tools.parseUrlRegEx.lastIndex = 0;
		var url = tools.parseUrlRegEx.exec(url);

		if (url) {
			//var protocol = url[1] || null;
			if (protocol) {
				protocol = protocol.toLowerCase().substr(0, protocol.length - 3);
			};
			var login = url[1] || null,
				user = null,
				password = null,
				domain = url[2] || null,
				port = url[3] || null,
				path = url[4] || null,
				file = null,
				args = url[5] || null,
				anchor = url[6] || null,
				isWindows = false;
			if (!protocol && domain && !file) {
				file = domain;
				domain = null;
			};
			if (protocol === 'file') {
				path = (login || '') + (domain || '') + (port || '') + (path || '');
				login = null;
				domain = null;
				port = null;
				var pos1 = path.indexOf('/', 1),
					pos2 = path.indexOf(':');
				if ((pos2 >= 0) && ((pos1 < 0) || (pos2 < pos1))) {
					isWindows = true;
				};
			} else {
				if (domain) {
					if (login) {
						login = login.substr(0, login.length - 1).split(':', 2);
						user = login[0] || '';
						if (removeEscapes) {
							user = unescape(user);
						};
						password = login[1];
						if (password === undefined) {
							password = null;
						} else if (removeEscapes) {
							password = unescape(password);
						};
					};
					domain = domain.toLowerCase();
					if (port) {
						port = parseInt(port.substr(1));
					};
				} else {
					user = null;
					password = null;
					port = null;
				};
			};
			if (path) {
				pos = path.lastIndexOf('/');
				if (pos >= 0) {
					file = path.substring(pos + 1) || null;
					path = path.substring(0, pos + 1);
				};
				if (!isAbsolute) {
					path = tools.relativeToAbsolutePath(null, path).path;
				};
				if (removeEscapes) {
					path = unescape(path);
				};
			};
			if (file) {
				if (!isAbsolute) {
					file = tools.relativeToAbsolutePath(path, file);
					path = file.basePath;
					file = file.path;
				};
				if (file && removeEscapes) {
					file = unescape(file);
				};
			};
			if (args) {
				args = args.substr(1).split('&');
				for (var i = 0; i < args.length; i++) {
					var arg = args[i].split('=', 2);
					if (removeEscapes) {
						arg[0] = unescape(arg[0]);
					};
					if (removeEscapes && (arg.length === 2)) {
						arg[1] = unescape(arg[1]);
					};
					args[i] = {
						name: arg[0],
						value: arg[1],
					};
				};
			};
			if (anchor) {
				anchor = anchor.substr(1);
				if (removeEscapes) {
					anchor = unescape(anchor);
				};
			};
			return new tools.url(
				/*protocol*/ protocol,
				/*user*/ user,
				/*password*/ password,
				/*domain*/ domain,
				/*port*/ port,
				/*path*/ path,
				/*file*/ file,
				/*args*/ args,
				/*anchor*/ anchor,
				/*isAbsolute*/ !!isAbsolute,
				/*isEscaped*/ !removeEscapes,
				/*isWindows*/ isWindows
			);
		};
		
		return url;
	};

	 tools.stackToString = function() {
		var str = '';
		for (var i = 0; i < this.length; i++) {
			var trace = this[i];
			str += (i + ': function "' + (trace.functionName || '<unknown>') + '" in file "' + (trace.fileName || '<unknown>') + '" at line ' + (trace.lineNumber < 0 ? '<unknown>' : trace.lineNumber) + ', column ' + (trace.columnNumber < 0 ? '<unknown>' : trace.columnNumber) + '\n');
		};
		return str;
	};
	tools.parseStackRegEx = /([^(@]+[(@])?(([a-zA-Z]+[:][/][/][/]?[^/]+[/][^: ]+)|(eval code))( line ([0-9]+) [>] eval)?(([:])([0-9]+)([:])([0-9]+))/gm;
	tools.parseStack = function parseStack(ex) {
		doodad.ASSERT && doodad.ASSERT(tools.isError(ex) || tools.isString(ex), "Invalid error object or stack trace.");

		var stack = ex;
		if (tools.isError(ex)) {
			stack = (ex.stack || '');
		};
		
		// Google Chrome 39: "Error\n    at Object.Doodad.Tools.getCurrentScript (file:///F:/Doodad/test.html:38:72)\n    at file:///F:/Doodad/debug.js:17:25"
		// Firefox 34: "Doodad.Tools.getCurrentScript<@file:///F:/Doodad/test.html:38:72@file:///F:/Doodad/debug.js:17:18"
		// IE 11: "Error\n   at Doodad.Tools.getCurrentScript (file:///F:/Doodad/test.html:38:66)\n   at Global code (file:///F:/Doodad/debug.js:7:1)"
		// Opera 26: "Error: file:///F:/Doodad/debug.js\n    at Object.Doodad.Tools.getCurrentScript (file:///F:/Doodad/test.html:37:78)\n    at file:///F:/Doodad/debug.js:33:21"
		// Safari 5 (win): undefined
		
		// NOTE: Internet Explorer 11 doesn't return more than 10 call levels.
		
		var functionName, 
			pos,
			calls = [];
		
		tools.parseStackRegEx.lastIndex = 0;
		var call = tools.parseStackRegEx.exec(stack);
		while (call) {
			functionName = call[1] || '';
			pos = functionName.indexOf(' at '); // Not Firefox beginning of function name
			if (pos >= 0) {
				functionName = functionName.substr(pos + 4);
			};
			pos = functionName.lastIndexOf('/'); // Firefox namespace
			if (pos >= 0) {
				functionName = functionName.substr(pos + 1);
			};
			pos = functionName.lastIndexOf('@'); // Firefox beginning of file name
			if (pos >= 0) {
				functionName = functionName.substring(0, pos);
			} else {
				pos = functionName.lastIndexOf('('); // Not Firefox beginning of file name
				if (pos >= 0) {
					functionName = functionName.substring(0, pos);
				};
			};
			functionName = functionName.trim();
			calls.push({
				functionName: ((functionName === "eval code") ? '' : functionName),
				fileName: (call[3] || ''),
				url: tools.parseUrl(call[3] || ''),
				lineNumber: parseInt(call[6] || call[9] || -1),
				columnNumber: parseInt(call[11] || -1),
			});
			call = tools.parseStackRegEx.exec(stack);
		};
		
		calls.toString = tools.stackToString;
		
		return calls;
	};
	
	tools.getStackTrace = function getStackTrace() {
		try {
			throw new Error("");
		} catch(ex) {
			var stack = tools.parseStack(ex);
			if (stack.length) {
				stack.splice(0, 1);
			} else {
				var caller;
				try {
					caller = arguments.callee.caller;
					while (caller) {
						var fn = caller.toString(),
							pos = fn.indexOf("function ") + 9;
						if (pos >= 0) {
							fn = fn.substring(pos, fn.indexOf('(')).trim();
						} else {
							fn = null;
						};
						stack.push({
							functionName: fn || '<anonymous>',
							url: null,
							lineNumber: -1,
							columnNumber: -1,
						});
						caller = caller.caller;
					};
				} catch(ex) {
				};
			};
			return stack;
		};
	};

	tools.getCurrentScript = function getCurrentScript(currentScript) {
		// Usage: Doodad.Tools.getCurrentScript(document.currentScript||(function(){try{throw new Error("")}catch(ex){return ex}})())
	
		var url = null,
			stack;
		
		if (currentScript) {
			if (tools.isString(currentScript.src)) {
				// currentScript = document.currentScript
				// Google Chrome 39 : "file:///F:/Doodad/debug.js"
				// Firefox 34: undefined
				// IE 11: undefined
				// Opera 26: "file:///F:/Doodad/debug.js"
				// Safari 5: undefined
				url = tools.parseUrl(currentScript.src);
			} else if (tools.isString(currentScript.fileName)) {
				// currentScript = throw new Error("");
				// Google Chrome 39 : undefined
				// Firefox 35: "file:///F:/Doodad/debug.js"
				// IE 11: undefined
				// Opera 26: undefined
				// Safari 5: undefined
				url = tools.parseUrl(currentScript.fileName);
			} else if (tools.isString(currentScript.sourceURL)) {
				// currentScript = throw new Error("");
				// Google Chrome 39 : undefined
				// Firefox 34: undefined
				// IE 11: undefined
				// Opera 26: undefined
				// Safari 5: "file:///F:/Doodad/debug.js"
				url = tools.parseUrl(currentScript.sourceURL);
			} else if (tools.isString(currentScript.stack)) {
				// currentScript = throw new Error("");
				// Google Chrome 39 : undefined
				// Firefox 34: undefined
				// IE 11: undefined
				// Opera 26: undefined
				// Safari 5: "file:///F:/Doodad/debug.js"
				stack = tools.parseStack(currentScript.stack);
				if (stack.length) {
					url = stack[0].url;
				};
			};
		};
		
		if (!url) {
			stack = tools.getStackTrace();
			if (stack.length > 1) {
				url = stack[1].url;
			};
		};
		
		return url;
	};
	
	
	tools.compareNumbers = function compare(value1, value2) {
		// Usable comparer function
		// NOTE: This changes the default behavior of "sort" so "null/NaN/String/Object/Array" are all evaluated to 0 and "-1 is > -Infinity"
		// null/NaN/undefined/String/Object/Array  are   0
		value1 = ((+value1 === value1) ? value1 : (value1 | 0));
		value2 = ((+value2 === value2) ? value2 : (value2 | 0));
		if (value1 > value2) {
			return 1;
		} else if (value1 < value2) {
			return -1;
		} else {
			return 0;
		};
	};
	tools.compareNumbersInverted = function compareInverted(value2, value1) {
		// Usable comparer function
		// null/NaN/undefined/String/Object/Array  are   0
		value1 = ((+value1 === value1) ? value1 : (value1 | 0));
		value2 = ((+value2 === value2) ? value2 : (value2 | 0));
		if (value1 > value2) {
			return 1;
		} else if (value1 < value2) {
			return -1;
		} else {
			return 0;
		};
	};

	
	tools.array = function array(obj) {
		return __arrayConstructor__.apply(null, obj);
	};
	

	tools.isClonable = function isClonable(obj, /*optional*/cloneFunctions) {
		// NOTE: This function will get replaced when "Doodad.js" is loaded.
		return !tools.isString(obj) && (tools.isArrayLike(obj) || tools.isObject(obj) || (!!cloneFunctions && tools.isFunction(val) && !tools.isNativeFunction(val)));
	};
	tools.clone = function clone(obj, /*optional*/depth, /*optional*/cloneFunctions) {
		// NOTE: This function will get replaced when "Doodad.js" is loaded.
		var result;

		if (tools.isNothing(obj)) {
			result = obj;
		} else {
			depth = (+depth || 0) - 1;  // null|undefined|true|false|NaN|Infinity
			cloneFunctions = (+cloneFunctions || 0) - 1;  // null|undefined|true|false|NaN|Infinity
			
			var key;
			
			if (tools.isClonable(obj)) {
				if (tools.isArrayLike(obj)) {
					obj = Object(obj);
					if (depth >= 0) {
						result = Array(obj.length);
						for (key = 0; key < obj.length; key++) {
							if (key in obj) {
								result[key] = tools.clone(obj[key], depth, cloneFunctions);
							};
						};
					} else {
						result = __arraySlice__.call(obj, 0);
					};
				} else if (tools.isFunction(obj)) {
					if (cloneFunctions >= 0) {
						result = eval('(' + __functionToString__.call(obj) + ')');
					} else {
						return obj;
					};
				} else {  // if (tools.isObject(obj))
					result = {};
				};
			} else {
				return obj;
			};

			// Copy properties
			if (depth >= 0) {
				// "tools.extend"
				var keys = tools.keys(obj),
					keysLen = keys.length, // performance
					i;
				for (i = 0; i < keysLen; i++) {
					key = keys[i];
					result[key] = tools.clone(obj[key], depth, cloneFunctions);
				};
			} else {
				result = tools.extend(result, obj);
			};
		};
		
		return result;
	};
	

	tools.hasIndex = function hasIndex(obj, index) {
		if (tools.isArrayLike(obj)) {
			obj = Object(obj);
			return !isNaN(index) && isFinite(index) && (index in obj);
		} else {
			doodad.ASSERT && doodad.ASSERT(false, "Invalid array.");
			return false;
		};
	};
	tools.hasIndexes = function hasIndexes(obj, indexes) {
		if (tools.isNothing(obj)) {
			return false;
		} else if (tools.isArrayLike(obj)) {
			obj = Object(obj);
			var len = obj.length;
			for (var i = 0; i < indexes.length; i++) {
				if (i in indexes) {
					var index = indexes[i];
					if (isNaN(index) || !isFinite(key) || !(index in obj)) {
						return false;
					};
				};
			};
			return true;
		} else {
			doodad.ASSERT && doodad.ASSERT(false, "Invalid array.");
			return false;
		};
	};
	tools.hasKey = function hasKey(obj, key) {
		if (!tools.isNothing(obj)) {
			if (__objectHasOwnProperty__.call(obj, key)) {
				return true;
			};
		};
		return false;
	};
	tools.hasKeys = function hasKeys(obj, keys) {
		doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(keys), "Invalid keys array");

		if (!tools.isNothing(obj)) {
			obj = Object(obj);
			keys = Object(keys);
			
			for (var i = 0; i < keys.length; i++) {
				if (i in keys) {
					var key = keys[i];
					if (!__objectHasOwnProperty__.call(obj, key)) {
						return false;
					};
				};
			};
			
			return true;
		};
		
		return false;
	};
	tools.indexes = function indexes(obj) {
		var result = [];
		if (tools.isArrayLike(obj)) {
			obj = Object(obj);
			for (var key = 0; key < obj.length; key++) {
				if (key in obj) {
					result.push(key);
				};
			};
		};
		return result;
	};
	tools.keys = function keys(obj) {
		// Returns enumerable own properties (those not inherited).
		// Doesn't not include array items.
		var result;
		if (tools.isArrayLike(obj)) {
			result = [];
			var len = obj.length, // performance
				key;
			for (key in obj) {
				if ((isNaN(key) || !isFinite(key)) && __objectHasOwnProperty__.call(obj, key)) {
					result.push(key);
				};
			};
		} else if (__objectKeys__) {
			 result = __objectKeys__(obj);
		} else {
			// Polyfill from Mozilla Developer Network.
			obj = Object(obj);
			var result = [],
				key, i;
			for (key in obj) {
				if (__objectHasOwnProperty__.call(obj, key)) {
					result.push(key);
				};
			};
		};
		if (__hasDontEnumBug__) {
			for (i = 0; i < __defaultNonEnumerablesLen__; i++) {
				key = __defaultNonEnumerables__[i];
				if (__objectHasOwnProperty__.call(obj, key)) {
					result.push(key);
				};
			};
		};
		return result;
	};
	tools.ownKeys = (__objectGetOwnPropertyNames__ ? __objectGetOwnPropertyNames__ : function ownKeys(obj) {
		// Returns enumerable and non-enumerable own properties (those not inherited).
		// Does include array items.
		obj = Object(obj);
		var result = [];
		var key, i;
		if (tools.isArrayLike(obj)) {
			for (key in obj) {
				if (__objectHasOwnProperty__.call(obj, key)) {
					result.push(key);
				};
			};
		} else {
			for (key in obj) {
				if ((isNaN(key) || !isFinite(key)) && __objectHasOwnProperty__.call(obj, key)) {
					result.push(key);
				};
			};
		};
		for (i = 0; i < __defaultNonEnumerablesLen__; i++) {
			// NOTE: Can only returns non-enumerable attributes in "__defaultNonEnumerables__"
			key = __defaultNonEnumerables__[i];
			if (__objectHasOwnProperty__.call(obj, key)) {
				result.push(key);
			};
		};
		return result;
	});
	tools.allKeys = function allKeys(obj) {
		// Returns all enumerable properties (inherited and not inherited).
		// Does include array items.
		obj = Object(obj);
		var result = [],
			key, i;
		for (key in obj) {
			result.push(key);
		};
		if (__hasDontEnumBug__) {
			// NOTE: Will return attributes in "__defaultNonEnumerables__" even when they are really non-enumerable. Unfortunately, "Object.propertyIsEnumerable" don't returns "true" when attribute is inherited and enumerable.
			for (i = 0; i < __defaultNonEnumerablesLen__; i++) {
				key = __defaultNonEnumerables__[i];
				if (key in obj) {
					result.push(key);
				};
			};
		};
		return result;
	};
	tools.values = function values(obj) {
		if (tools.isNothing(obj)) {
			return [];
		} else {
			obj = Object(obj);
			var result = [];
			if (tools.isArrayLike(obj)) {
				for (var key = 0; key < obj.length; key++) {
					if (key in obj) {
						result.push(obj[key]);
					};
				};
			} else {
				var keys = tools.keys(obj),
					keysLen = keys.length; // performance
				for (var i = 0; i < keysLen; i++) {
					result.push(obj[keys[i]]);
				};
			};
			return result;
		};
	};
	tools.items = function items(obj) {
		if (tools.isNothing(obj)) {
			return [];
		} else {
			obj = Object(obj);
			var result = [];
			if (tools.isArrayLike(obj)) {
				for (var key = 0; key < obj.length; key++) {
					if (key in obj) {
						result.push([key, obj[key]]);
					};
				};
			} else {
				var keys = tools.keys(obj),
					keysLen = keys.length; // performance
				for (var i = 0; i < keysLen; i++) {
					var key = keys[i];
					result.push([key, obj[key]]);
				};
			};
			return result;
		};
	};
	tools.getFirstIndex = function getFirstIndex(obj) {
		if (tools.isArrayLike(obj)) {
			for (var key = 0; key < obj.length; key++) {
				if (key in obj) {
					return key;
				};
			};
		};
	};
	tools.getFirstValue = function getFirstValue(obj) {
		if (tools.isArrayLike(obj)) {
			for (var key = 0; key < obj.length; key++) {
				if (key in obj) {
					return obj[key];
				};
			};
		};
	};
	tools.getDefault = function getDefault(obj, key, /*optional*/defaultValue) {
		if (tools.isNothing(obj)) {
			doodad.ASSERT && doodad.ASSERT(false, "Invalid object.");
			return defaultValue;
		} else if (tools.hasKey(obj, key)) {
			return obj[key];
		} else {
			obj[key] = defaultValue;
			return defaultValue;
		};
	};

	
	tools.findItem = function findItem(obj, item, /*optional*/thisObj, /*optional*/includeFunctions) {
		if (!tools.isNothing(obj)) {
			var key;
			if (!includeFunctions && tools.isFunction(item)) {
				if (tools.isArrayLike(obj)) {
					for (key = 0; key < obj.length; key++) {
						if (key in obj) {
							if (item.call(thisObj, obj[key], key, obj)) {
								return key;
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						if (item.call(thisObj, obj[key], key, obj)) {
							return key;
						};
					};
				};
			} else {
				if (tools.isArrayLike(obj)) {
					for (key = 0; key < obj.length; key++) {
						if (key in obj) {
							if (obj[key] === item) {
								return key;
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						if (obj[key] === item) {
							return key;
						};
					};
				};
			};
		};
		return null;
	};
	tools.findLastItem = function findLastItem(obj, item, /*optional*/thisObj, /*optional*/includeFunctions) {
		if (!tools.isNothing(obj)) {
			var key;
			if (!includeFunctions && tools.isFunction(item)) {
				if (tools.isArrayLike(obj)) {
					for (key = obj.length - 1; key >= 0; key--) {
						if (key in obj) {
							if (item.call(thisObj, obj[key], key, obj)) {
								return key;
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = keysLen - 1; i >= 0; i--) {
						key = keys[i];
						if (item.call(thisObj, obj[key], key, obj)) {
							return key;
						};
					};
				};
			} else {
				if (tools.isArrayLike(obj)) {
					for (key = obj.length - 1; key >= 0; key--) {
						if (key in obj) {
							if (obj[key] === item) {
								return key;
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = keysLen - 1; i >= 0; i--) {
						key = keys[i];
						if (obj[key] === item) {
							return key;
						};
					};
				};
			};
		};
		return null;
	};
	tools.findItems = function findItems(obj, items, /*optional*/thisObj, /*optional*/includeFunctions) {
		var result = [];
		if (!tools.isNothing(obj)) {
			var key;
			if (!includeFunctions && tools.isFunction(items)) {
				if (tools.isArrayLike(obj)) {
					for (key = 0; key < obj.length; key++) {
						if (key in obj) {
							if (items.call(thisObj, obj[key], key, obj)) {
								result.push(key);
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						if (items.call(thisObj, obj[key], key, obj)) {
							result.push(key);
						};
					};
				};
			} else {
				if (tools.isArrayLike(obj)) {
					for (key = 0; key < obj.length; key++) {
						if (key in obj) {
							if (tools.findItem(items, obj[key], undefined, true) !== null) {
								result.push(key);
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						if (tools.findItem(items, obj[key], undefined, true) !== null) {
							result.push(key);
						};
					};
				};
			};
		};
		return result;
	};
	tools.popAt = function popAt(obj, key) {
		if (tools.isNothing(obj)) {
			doodad.ASSERT && doodad.ASSERT(false, "Invalid object.");
		} else if (tools.isArray(obj)) {
			if (key in obj) {
				var item = obj[key];
				__arraySplice__.call(obj, key, 1);
				return item;
			};
		} else {
			obj = Object(obj);
			if (__objectHasOwnProperty__.call(obj, key)) {
				var item = obj[key];
				delete obj[key];
				return item;
			};
		};
	};
	tools.popItem = function popItem(obj, item, /*optional*/thisObj, /*optional*/includeFunctions) {
		if (!tools.isNothing(obj)) {
			var key, 
				val;
			if (!includeFunctions && tools.isFunction(item)) {
				if (tools.isArray(obj)) {
					for (key = 0; key < obj.length; key++) {
						if (key in obj) {
							val = obj[key];
							if (item.call(thisObj, val, key, obj)) {
								__arraySplice__.call(obj, key, 1);
								return val;
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (item.call(thisObj, val, key, obj)) {
							delete obj[key];
							return val;
						};
					};
				};
			} else {
				if (tools.isArray(obj)) {
					for (key = 0; key < obj.length; key++) {
						if (key in obj) {
							val = obj[key];
							if (val === item) {
								__arraySplice__.call(obj, key, 1);
								return val;
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (val === item) {
							delete obj[key];
							return val;
						};
					};
				};
			};
		};
		return null;
	};
	tools.popItems = function popItems(obj, items, /*optional*/thisObj, /*optional*/includeFunctions) {
		var result = [];
		if (!tools.isNothing(obj)) {
			var key, 
				val;
				
			if (!includeFunctions && tools.isFunction(items)) {
				if (tools.isArray(obj)) {
					for (key = obj.length - 1; key >= 0; key--) {
						if (key in obj) {
							val = obj[key];
							if (items.call(thisObj, val, key, obj)) {
								__arraySplice__.call(obj, key, 1);
								result.push(val);
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (items.call(thisObj, val, key, obj)) {
							delete obj[key];
							result.push(val);
						};
					};
				};
			} else {
				if (tools.isArray(obj)) {
					for (key = obj.length - 1; key >= 0; key--) {
						if (key in obj) {
							val = obj[key];
							if (tools.findItem(items, val, undefined, true) !== null) {
								__arraySplice__.call(obj, key, 1);
								result.push(val);
							};
						};
					};
				} else {
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (tools.findItem(items, val, undefined, true) !== null) {
							delete obj[key];
							result.push(val);
						};
					};
				};
			};
		};
		return result;
	};
	tools.extend = function extend(/*paramarray*/obj) {
		var result;
		if (!tools.isNothing(obj)) {
			result = Object(obj);
			for (var i = 1; i < arguments.length; i++) {
				obj = arguments[i];
				if (tools.isNothing(obj)) {
					continue;
				};
				if (!tools.isArrayLike(obj) && __objectAssign__) {
					result = __objectAssign__(result, obj);
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						j, 
						key;
					for (j = 0; j < keysLen; j++) {
						key = keys[j];
						result[key] = obj[key];
					};
				};
			};
		};
		
		return result;
	};
	tools.depthExtend = function depthExtend(depth, /*paramarray*/obj) {
		var result = obj;
		if (!tools.isNothing(obj)) {
			depth = (+depth || 0) - 1;  // null|undefined|true|false|NaN|Infinity
			if (depth >= 0) {
				result = Object(result);
				for (var i = 2; i < arguments.length; i++) {
					obj = arguments[i];
					if (tools.isNothing(obj)) {
						continue;
					};
					// "Object.assign" Polyfill from Mozilla Developer Network.
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						j, 
						key, 
						objVal,
						resultVal;
					for (j = 0; j < keysLen; j++) {
						key = keys[j];
						objVal = obj[key];
						if (tools.isObjectLike(objVal)) {
							resultVal = result[key];
							if (tools.isNothing(resultVal)) {
								result[key] = tools.depthExtend(depth, {}, objVal);
							} else if (tools.isObjectLike(resultVal)) {
								tools.depthExtend(depth, resultVal, objVal);
							};
						} else {
							result[key] = objVal;
						};
					};
				};
			} else {
				result = tools.extend.apply(tools, __arraySlice__.call(arguments, 1));
			};
		};

		return result;
	};
	tools.append = function append(obj /*paramarray*/) {
		if (!tools.isArrayLike(obj)) {
			doodad.ASSERT && doodad.ASSERT(false, "Invalid array.");
			return null;
		};
		
		var result,
			start = 0;
		if (tools.isArray(obj)) {
			result = obj;
			start = 1;
		} else {
			result = [];
		};
		
		for (var i = start; i < arguments.length; i++) {
			obj = arguments[i];
			if (tools.isNothing(obj)) {
				continue;
			};
			doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");
			obj = Object(obj);
			__arrayPush__.apply(result, obj);
		};
		
		return result;
	};
	tools.prepend = function prepend(obj /*paramarray*/) {
		if (!tools.isArrayLike(obj)) {
			doodad.ASSERT && doodad.ASSERT(false, "Invalid array.");
			return null;
		};
		
		var result,
			start = 0;
		if (tools.isArray(obj)) {
			result = obj;
			start = 1;
		} else {
			result = [];
		};
		
		for (var i = start; i < arguments.length; i++) {
			obj = arguments[i];
			if (tools.isNothing(obj)) {
				continue;
			};
			doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");
			obj = Object(obj);
			__arrayUnshift__.apply(result, obj);
		};
		
		return result;
	};
	tools.indexOf = function indexOf(obj, item, /*optional*/from) {
		if (tools.isArrayLike(obj)) {
			if (tools.isNothing(from)) {
				from = 0;
			} else if (!tools.isInteger(from)) {
				doodad.ASSERT && doodad.ASSERT(false, "Invalid number");
				from = 0;
			};
			if (tools.isString(obj)) {
				return __stringIndexOf__.call(obj, item, from);
			} else {
				if (__arrayIndexOf__) {
					// JS 1.6
					return __arrayIndexOf__.call(obj, item, from);
				} else {
					obj = Object(obj);
					var len = obj.length;
					from = Math.max(from >= 0 ? from : len - Math.abs(from), 0);
					for (var key = from; key < len; key++) {
						if (key in obj) {
							if (obj[key] === item) {
								return key;
							};
						};
					};
				};
			};
		};
		return -1;
	};
	tools.lastIndexOf = function lastIndexOf(obj, item, /*optional*/from) {
		if (tools.isArrayLike(obj)) {
			var len = obj.length;
			if (tools.isNothing(from)) {
				from = len - 1;
			} else {
				doodad.ASSERT && doodad.ASSERT(tools.isInteger(from), "Invalid number");
			};
			if (tools.isString(obj)) {
				return __stringLastIndexOf__.call(obj, item, from);
			} else {
				if (__arrayLastIndexOf__) {
					// JS 1.6
					return __arrayLastIndexOf__.call(obj, item, from);
				} else {
					obj = Object(obj);
					from = Math.min(from >= 0 ? from : len - Math.abs(from), len - 1);
					for (var key = len - 1; key >= from; key--) {
						if (key in obj) {
							if (obj[key] === item) {
								return key;
							};
						};
					};
				};
			};
		};
		return -1;
	};
	tools.available = function available(obj) {
		if (tools.isArrayLike(obj)) {
			obj = Object(obj);
			for (var key = 0; key < obj.length; key++) {
				if (!(key in obj)) {
					return key;
				};
			};
		};
		return -1;
	};
	tools.availables = function availables(obj) {
		var keys = [];
		if (tools.isArrayLike(obj)) {
			obj = Object(obj);
			for (var key = 0; key < obj.length; key++) {
				if (!(key in obj)) {
					keys.push(key);
				};
			};
		};
		return keys;
	};
	tools.map = function map(obj, fn, /*optional*/thisObj) {
		doodad.ASSERT && doodad.ASSERT(tools.isFunction(fn), "Invalid function");

		if (!tools.isNothing(obj)) {
			doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");

			if (__arrayMap__) {
				return __arrayMap__.call(obj, fn, thisObj);
			} else {
				obj = Object(obj);
				var result = Array(obj.length);
				for (var key = 0; key < obj.length; key++) {
					if (key in obj) {
						result[key] = fn.call(thisObj, obj[key], key, obj);
					};
				};
				return result;
			};
		};
	};
	tools.forEach = function forEach(obj, fn, /*optional*/thisObj) {
		doodad.ASSERT && doodad.ASSERT(tools.isFunction(fn), "Invalid function");
		
		if (!tools.isNothing(obj)) {
			if (tools.isArrayLike(obj)) {
				if (__arrayForEach__) {
					// JS 1.6
					return __arrayForEach__.call(obj, fn, thisObj);
				} else {
					obj = Object(obj);
					for (var key = 0; key < obj.length; key++) {
						if (key in obj) {
							fn.call(thisObj, obj[key], key, obj);
						};
					};
				};
			} else {
				// "Object.assign" Polyfill from Mozilla Developer Network.
				obj = Object(obj);
				var keys = tools.keys(obj),
					keysLen = keys.length, // performance
					i, 
					key;
				for (i = 0; i < keysLen; i++) {
					key = keys[i];
					fn.call(thisObj, obj[key], key, obj);
				};
			};
		};
	};
	tools.filter = function filter(obj, items, /*optional*/thisObj, /*optional*/invert, /*optional*/includeFunctions) {
		var result;
		if (!tools.isNothing(obj)) {
			obj = Object(obj);
			invert = !!invert;
			var keys,
				keysLen,
				i, 
				key,
				val;
			if (!includeFunctions && tools.isFunction(items)) {
				if (tools.isArrayLike(obj)) {
					if (__arrayFilter__ && !invert) {
						// JS 1.6
						result = __arrayFilter__.call(obj, items, thisObj);
					} else {
						result = [];
						for (var key = 0; key < obj.length; key++) {
							if (key in obj) {
								var val = obj[key];
								if (invert === !items.call(thisObj, val, key, obj)) {
									result.push(val);
								};
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					result = {};
					keys = tools.keys(obj);
					keysLen = keys.length;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (invert === !items.call(thisObj, val, key, obj)) {
							result[key] = val;
						};
					};
				};
			} else {
				if (tools.isArrayLike(obj)) {
					result = [];
					for (var key = 0; key < obj.length; key++) {
						if (key in obj) {
							val = obj[key];
							if (invert === (tools.findItem(items, val, undefined, true) === null)) {
								result.push(val);
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					result = {};
					keys = tools.keys(obj);
					keysLen = keys.length;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (invert === (tools.findItem(items, val, undefined, true) === null)) {
							result[key] = val;
						};
					};
				};
			};
		};
		return result;
	};
	tools.filterKeys = function filterKeys(obj, items, /*optional*/thisObj, /*optional*/invert, /*optional*/includeFunctions) {
		var result;
		if (!tools.isNothing(obj)) {
			obj = Object(obj);
			invert = !!invert;
			var keys,
				keysLen,
				i, 
				key,
				val;
			if (!includeFunctions && tools.isFunction(items)) {
				if (tools.isArrayLike(obj)) {
					result = [];
					for (var key = 0; key < obj.length; key++) {
						if (key in obj) {
							var val = obj[key];
							if (invert === !items.call(thisObj, key, val, obj)) {
								result.push(val);
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					result = {};
					keys = tools.keys(obj);
					keysLen = keys.length;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (invert === !items.call(thisObj, key, val, obj)) {
							result[key] = val;
						};
					};
				};
			} else {
				if (tools.isArrayLike(obj)) {
					result = [];
					for (var key = 0; key < obj.length; key++) {
						if (key in obj) {
							val = obj[key];
							if (invert === (tools.findItem(items, key, undefined, true) === null)) {
								result.push(val);
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					result = {};
					keys = tools.keys(obj);
					keysLen = keys.length;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (invert === (tools.findItem(items, key, undefined, true) === null)) {
							result[key] = val;
						};
					};
				};
			};
		};
		return result;
	};
	tools.every = function every(obj, /*optional*/fn, /*optional*/thisObj, /*optional*/includeFunctions) {
		if (!tools.isNothing(obj)) {
			if (!includeFunctions && tools.isFunction(fn)) {
				if (tools.isArrayLike(obj)) {
					if (__arrayEvery__) {
						// JS 1.6
						return __arrayEvery__.call(obj, fn, thisObj);
					} else {
						obj = Object(obj);
						for (var key = 0; key < obj.length; key++) {
							if (key in obj) {
								var value = obj[key];
								if (!fn.call(thisObj, value, key, obj)) {
									return false;
								};
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i, 
						key, 
						val;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (!fn.call(thisObj, val, key, obj)) {
							return false;
						};
					};
				};
			} else {
				obj = Object(obj);
				if (tools.isArrayLike(obj)) {
					for (var key = 0; key < obj.length; key++) {
						if (key in obj) {
							var value = obj[key];
							if (tools.findItem(fn, val, undefined, true) === null) {
								return false;
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i, 
						key, 
						val;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (tools.findItem(fn, val, undefined, true) === null) {
							return false;
						};
					};
				};
			};
		};
		return true;
	};
	tools.some = function some(obj, /*optional*/fn, /*optional*/thisObj, /*optional*/includeFunctions) {
		if (!tools.isNothing(obj)) {
			if (!includeFunctions && tools.isFunction(fn)) {
				if (tools.isArrayLike(obj)) {
					if (__arraySome__) {
						// JS 1.6
						return __arraySome__.call(obj, fn, thisObj);
					} else {
						obj = Object(obj);
						for (var key = 0; key < obj.length; key++) {
							if (key in obj) {
								var value = obj[key];
								if (fn.call(thisObj, value, key, obj)) {
									return true;
								};
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i, 
						key, 
						val;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (fn.call(thisObj, val, key, obj)) {
							return true;
						};
					};
				};
			} else {
				obj = Object(obj);
				if (tools.isArrayLike(obj)) {
					for (var key = 0; key < obj.length; key++) {
						if (key in obj) {
							var value = obj[key];
							if (tools.findItem(fn, val, undefined, true) !== null) {
								return true;
							};
						};
					};
				} else {
					// "Object.assign" Polyfill from Mozilla Developer Network.
					obj = Object(obj);
					var keys = tools.keys(obj),
						keysLen = keys.length, // performance
						i, 
						key, 
						val;
					for (i = 0; i < keysLen; i++) {
						key = keys[i];
						val = obj[key];
						if (tools.findItem(fn, val, undefined, true) !== null) {
							return true;
						};
					};
				};
			};
		};
		return false;
	};
	tools.reduce = function reduce(obj, fn, /*optional*/initialValue) {
		doodad.ASSERT && doodad.ASSERT(tools.isFunction(fn), "Invalid function.");

		if (tools.isNothing(obj)) {
			return initialValue;
		} else {
			doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");
			
			if (__arrayReduce__) {
				// JS 1.8
				if (arguments.length > 2) {
					return __arrayReduce__.call(obj, fn, initialValue);
				} else {
					return __arrayReduce__.call(obj, fn);
				};
			} else {
				obj = Object(obj);
				var result,
					key = 0,
					hasItem = false;
				if (arguments.length > 2) {
					result = initialValue;
					hasItem = true;
				} else {
					for (; key < obj.length; key++) {
						if (key in obj) {
							hasItem = true;
							result = obj[key];
							key++;
							break;
						};
					};
				};
				for (; key < obj.length; key++) {
					if (key in obj) {
						result = fn(result, obj[key], key, obj);
					};
				};
				if (!hasItem) {
					throw new TypeError("Reduce of empty array with no initial value.");
				};
				return result;
			};
		};
	};
	tools.reduceRight = function reduceRight(obj, fn, /*optional*/initialValue) {
		doodad.ASSERT && doodad.ASSERT(tools.isFunction(fn), "Invalid function.");

		if (tools.isNothing(obj)) {
			return initialValue;
		} else {
			doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");

			if (__arrayReduceRight__) {
				// JS 1.8
				if (arguments.length > 2) {
					return __arrayReduceRight__.call(obj, fn, initialValue);
				} else {
					return __arrayReduceRight__.call(obj, fn);
				};
			} else {
				obj = Object(obj);
				var value = initialValue,
					hasItem = false,
					key = obj.length - 1;
				for (; key >= 0; key--) {
					if (key in obj) {
						hasItem = true;
						if (arguments.length < 3) {
							value = 0;
						};
						value = fn(value, obj[key], key, obj);
						key--;
						break;
					};
				};
				if (!hasItem && (arguments.length < 3)) {
					throw new TypeError("Reduce of empty array with no initial value.");
				};
				for (; key >= 0; key--) {
					if (key in obj) {
						value = fn(value, obj[key], key, obj);
					};
				};
				return value;
			};
		};
	};
	tools.unique = function unique(/*optional*/comparer, /*paramarray*/obj) {
		// NOTE: Use it when each array mainly contains unique values.

		var start = 1;
		if (!tools.isFunction(comparer)) {
			comparer = null;
			start = 0;
		};
		
		var result = [];
		
		if (comparer) {
			for (var i = start; i < arguments.length; i++) {
				obj = arguments[i];
				if (tools.isNothing(obj)) {
					continue;
				};
				doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");
				obj = Object(obj);
				for (var key1 = 0; key1 < obj.length; key1++) {
					if (key1 in obj) {
						var value1 = obj[key1],
							found = false;
						for (var key2 = 0; key2 < result.length; key2++) {
							if (comparer(value1, result[key2])) {
								found = true;
								break;
							};
						};
						if (!found) {
							result.push(value1);
						};
					};
				};
			};
		} else {
			for (var i = start; i < arguments.length; i++) {
				obj = arguments[i];
				obj = arguments[i];
				if (tools.isNothing(obj)) {
					continue;
				};
				doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");
				obj = Object(obj);
				for (var key1 = 0; key1 < obj.length; key1++) {
					if (key1 in obj) {
						var value1 = obj[key1],
							found = false;
						for (var key2 = 0; key2 < result.length; key2++) {
							if (value1 === result[key2]) {
								found = true;
								break;
							};
						};
						if (!found) {
							result.push(value1);
						};
					};
				};
			};
		};
		
		return result;
	};
	tools.unique2 = function unique2(/*optional*/comparer, /*paramarray*/obj) {
		// NOTE: Use it when each array mainly contains repeated values.
	
		var start = 1;
		if (!tools.isFunction(comparer)) {
			comparer = null;
			start = 0;
		};
		
		var result = [],
			tmp = null,
			index = 0,
			argsLen = arguments.length;
			
		if (argsLen === 1) {
			tmp = Object(arguments[0]);
			doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(tmp), "Invalid array");
			
		} else if (argsLen > 1) {
			index = tools.reduce(arguments, function(res, val, key)  {
				if (key < start) {
					return res;
				};
				if (tools.isArrayLike(val)) {
					return res + val.length;
				} else {
					doodad.ASSERT && doodad.ASSERT(false, "Invalid array");
					return res;
				};
			}, 0);
			tmp = new Array(index);
		};
		
		if (comparer) {
			if (argsLen > 1) {
				for (var i = argsLen - 1; i >= start; i--) {
					obj = arguments[i];
					if (tools.isNothing(obj)) {
						continue;
					};
					doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");
					obj = Object(obj);
					for (var key1 = obj.length - 1; key1 >= 0; key1--) {
						if (key1 in obj) {
							var value1 = obj[key1],
								found = false;
							for (var key2 = key1 - 1; key2 >= 0; key2--) {
								if (key2 in obj) {
									if (comparer(value1, obj[key2])) {
										found = true;
										break;
									};
								};
							};
							if (!found) {
								index--;
								tmp[index] = value1;
							};
						};
					};
				};
			};
			for (var key1 = tmp.length - 1; key1 >= index; key1--) {
				if (key1 in tmp) {
					var value1 = tmp[key1],
						found = false;
					for (var key2 = key1 - 1; key2 >= index; key2--) {
						if (key2 in tmp) {
							if (comparer(value1, tmp[key2])) {
								found = true;
								break;
							};
						};
					};
					if (!found) {
						result.push(value1);
					};
				};
			};
		} else {
			if (argsLen > 1) {
				for (var i = argsLen - 1; i >= start; i--) {
					obj = arguments[i];
					if (tools.isNothing(obj)) {
						continue;
					};
					doodad.ASSERT && doodad.ASSERT(tools.isArrayLike(obj), "Invalid array.");
					obj = Object(obj);
					for (var key1 = obj.length - 1; key1 >= 0; key1--) {
						if (key1 in obj) {
							var value1 = obj[key1],
								found = false;
							for (var key2 = key1 - 1; key2 >= 0; key2--) {
								if (key2 in obj) {
									if (value1 === obj[key2]) {
										found = true;
										break;
									};
								};
							};
							if (!found) {
								index--;
								tmp[index] = value1;
							};
						};
					};
				};
			};
			for (var key1 = tmp.length - 1; key1 >= index; key1--) {
				if (key1 in tmp) {
					var value1 = tmp[key1],
						found = false;
					for (var key2 = key1 - 1; key2 >= index; key2--) {
						if (key2 in tmp) {
							if (value1 === tmp[key2]) {
								found = true;
								break;
							};
						};
					};
					if (!found) {
						result.push(value1);
					};
				};
			};
		};
		
		result.reverse();
		
		return result;
	};
	
	

	tools.CustomEvent = (__windowCustomEvent__ ? __windowCustomEvent__ : (__supportsNewOnEvent__ ? function() {
		var event = function CustomEvent(type, /*optional*/init) {
			var ev = new window.Event(type);
			ev.detail = (tools.isObject(init) ? init.detail : null);
			return ev;
		};

		// For "instanceof".
		event.prototype = tools.setPrototypeOf(event.prototype, window.Event.prototype);
		
		return event;
	} : function() {
		// Old browsers
		var event = function CustomEvent(type, /*optional*/init) {
			var ev = document.createEvent('Event'),
				canBubble = false,
				cancelable = false;
			if (tools.isObject(init)) {
				cancelable = !!init.cancelable;
				canBubble = !!init.bubbles;
				ev.detail = init.detail;
			};
			ev.initEvent(type, canBubble, cancelable) 
			return ev;
		};

		// For "instanceof".
		event.prototype = tools.setPrototypeOf(event.prototype, window.Event.prototype);

		return event;
	})());

	tools.CustomEventTarget = (function() {
		var eventTarget = function CustomEventTarget() {
			// TODO: What is the formal property name ? Where all listeners are stored ?
			tools.defineProperty(this, '__EVENT_LISTENERS__', {
				value: {},
			});
			return this;
		};
		if (tools.isFunction(window.EventTarget)) {
			// For "instanceof".
			eventTarget.prototype = tools.setPrototypeOf(eventTarget.prototype, window.EventTarget.prototype);
		};
		tools.extend(eventTarget.prototype, {
			__EVENT_LISTENERS__: null,
			addEventListener: function addEventListener(type, handler, /*optional*/useCapture, /*optional*/wantsUntrusted) {
				if (doodad.ASSERT) {
					doodad.ASSERT(this instanceof eventTarget, "Object must inherit 'Doodad.Tools.CustomEventTarget'.");
					doodad.ASSERT(tools.isStringAndNotEmpty(type), "Invalid event type.");
					doodad.ASSERT(tools.isFunction(handler), "Invalid event handler.");
				};
				
				type = type.toLowerCase();
				useCapture = !!useCapture;
				wantsUntrusted = (tools.isNothing(wantsUntrusted) ? true : !!wantsUntrusted);
				
				var listeners = this.__EVENT_LISTENERS__[type];
				if (!tools.isArray(listeners)) {
					this.__EVENT_LISTENERS__[type] = listeners = [];
				};
				if (tools.findItem(listeners, function(value) {
					return (value[0] === handler) && (value[1] === useCapture);
				}) === null) {
					listeners.push([handler, useCapture, wantsUntrusted]);
				};
			},
			removeEventListener: function removeEventListener(type, handler, /*optional*/useCapture) {
				if (doodad.ASSERT) {
					doodad.ASSERT(this instanceof eventTarget, "Object must inherit 'Doodad.Tools.CustomEventTarget'.");
					doodad.ASSERT(tools.isStringAndNotEmpty(type), "Invalid event type.");
					doodad.ASSERT(tools.isFunction(handler), "Invalid event handler.");
				};

				type = type.toLowerCase();
				
				if (tools.hasKey(this.__EVENT_LISTENERS__, type)) {
					var listeners = this.__EVENT_LISTENERS__[type];
					if (tools.isArray(listeners)) {
						if (tools.isNothing(useCapture)) {
							tools.popItems(listeners, function(value) {
								return (value[0] === handler);
							});
						} else {
							useCapture = !!useCapture;
							tools.popItems(listeners, function(value) {
								return (value[0] === handler) && (value[1] === useCapture);
							});
						};
					};
				};
			},
			dispatchEvent: function dispatchEvent(event) {
				// TODO: What should I do with "useCapture" ???
				// TODO: Implement "wantsUntrusted" ?
				
				if (doodad.ASSERT) {
					doodad.ASSERT(this instanceof eventTarget, "Object must inherit 'Doodad.Tools.CustomEventTarget'.");
					doodad.ASSERT(event instanceof window.Event, "Event must inherit 'window.CustomEvent' or 'Doodad.Tools.CustomEvent'.");
				};

				var type = event.type.toLowerCase();
				
				var res;
				
				if (tools.hasKey(this.__EVENT_LISTENERS__, type)) {
					var	listeners = this.__EVENT_LISTENERS__[type],
						listenersLen,
						listener;
					if (tools.isArray(listeners)) {
						listenersLen = listeners.length;
						for (var i = 0; i < listenersLen; i++) {
							listener = listeners[i];
							res = listener[0].call(this, event);
							if (res === false) {
								break;
							};
						};
					};
				};
				
				// TODO: Should I call it first ?
				type = 'on' + type;
				if ((res !== false) && tools.hasKey(this, type)) {
					var fn = this[type];
					if (tools.isFunction(fn)) {
						res = fn.call(this, event);
					};
				};
				return res;
			},
		});
		return eventTarget;
	})();

	tools.createEventTarget = function(/*optional*/objOrFn, /*optional*/name) {
		var fn;
		if (tools.isFunction(objOrFn)) {
			fn = objOrFn;
			objOrFn = null;
		} else if (tools.isStringAndNotEmptyTrim(name)) {
			fn = eval('(function ' + name.replace(/[.]/g, '_') + '() {' +
				'return tools.CustomEventTarget.call(this);' +
			'})');
		} else {
			fn = function () {
				return tools.CustomEventTarget.call(this);
			};
		};

		// For "instanceof".
		fn.prototype = tools.setPrototypeOf(fn.prototype, tools.CustomEventTarget.prototype);

		if (tools.isObject(objOrFn)) {
			tools.extend(fn.prototype, objOrFn);
		};

		return fn;
	};


	
	var __loadedScripts__ = {};
	
	tools.ScriptLoader = (function() {
		var loader = function Doodad_Tools_ScriptLoader(tag, target, /*optional*/timeout) {
			if (doodad.ASSERT) {
				doodad.ASSERT(tools.isStringAndNotEmpty(tag), "Invalid tag.");
				doodad.ASSERT(tools.isElement(target), "Invalid target.");
			};
			this.tag = tag;
			this.target = target;
			if (tools.isNothing(timeout)) {
				this.timeout = tools.defaultScriptTimeout;
			} else {
				this.timeout = timeout;
			};
			return tools.CustomEventTarget.call(this);
		}
		tools.extend(loader.prototype, {
			tag: null,
			element: null,
			target: null,
			timeout: 0,
			loadEv: null,
			async: false,
			launched: false,
			ready: false,
			failed: false,
			timedout: false,
			timeoutId: null,
			
			oninit: null,
			onloading: null,
			onload: null,
			onerror: null,
			
			__handleSuccess: function __handleSuccess(ev) {
				if ((this.loadEv !== 'readystatechange') || (this.element.readyState === 'loaded') || (this.element.readyState === 'complete')) {
					if (this.timeoutId) {
						clearTimeout(this.timeoutId);
						this.timeoutId = null;
					};
					if (!this.ready) {
						this.ready = true;
						this.dispatchEvent(new tools.CustomEvent('load'));
						tools.dispatchEvent(new tools.CustomEvent('scriptload', {
							detail: {
								loader: this,
							},
						}));
					};
				};
			},
			__handleError: function __handleError(ev) {
				if (this.timeoutId) {
					clearTimeout(this.timeoutId);
					this.timeoutId = null;
				};
				if (!this.ready) {
					this.ready = true;
					this.failed = true;
					this.dispatchEvent(new tools.CustomEvent('error'));
					tools.dispatchEvent(new tools.CustomEvent('scripterror', {
						detail: {
							loader: this,
						},
					}));
					if (this.element && this.element.parentNode) {
						this.element.parentNode.removeChild(this.element);
						this.element = null;
					};
				};
			},
			start: function() {
				if (!this.launched) {
					this.launched = true;
					
					this.element = document.createElement(this.tag);
					if ('onreadystatechange' in this.element) {
						// For previous versions of IE
						this.loadEv = 'readystatechange';
					} else {
						this.loadEv = 'load';
					};
					
					var self = this;
					
					// NOTE: Safari: "onload" doesn't raise for the 'link' tag
					this.element.addEventListener(this.loadEv, function(ev) {
						return self.__handleSuccess(ev);
					});
					
					// NOTE: IE and Safari: "onerror" doesn't raise, so we can't know if there was an error
					this.element.addEventListener('error', function(ev) {
						return self.__handleError(ev);
					});

					this.dispatchEvent(new tools.CustomEvent('init'));
					tools.dispatchEvent(new tools.CustomEvent('scriptinit', {
						detail: {
							loader: this,
						},
					}));

					if (this.async) {
						// Workaround for asynchronous script loading
						var appendElement = function appendElement() {
							self.target.appendChild(self.element);
						};
						setTimeout(appendElement, 1);
					} else {
						this.target.appendChild(this.element);
					};
					
					this.dispatchEvent(new tools.CustomEvent('loading'));
					tools.dispatchEvent(new tools.CustomEvent('scriptloading', {
						detail: {
							loader: this,
						},
					}));

					// <PRB> Safari: "onload" doesn't raise with the 'link' tag
					if (this.tag === 'link') {
						var url = (this.element.src || this.element.href);
						if (url) {
							url = tools.parseUrl(url);
						};
						if (url) {
							var waitDownload = document.createElement('img');
							waitDownload.addEventListener('error', function handleWaitDownload(ev) {
								if (waitDownload.parentNode) {
									waitDownload.parentNode.removeChild(waitDownload);
								};
								self.loadEv = 'load';
								self.__handleSuccess(ev);
							});
							
							waitDownload.style.display = 'none';
							url.anchor = 'sami'; // "#sami" is a documented IE trick to load source as text instead of its mime type, preventing a download prompt to the user
							
							document.body.appendChild(waitDownload);
							
							// NOTE: The "img" tag starts download as soon as "src" is set
							waitDownload.src = url.toString();
						};
					};
					
					if (this.timeout) {
						this.timeoutId = setTimeout(function handleTimeout(ev) {
							self.timeoutId = null;
							if (!self.ready) {
								self.timedout = true;
								self.__handleError(ev);
							};
						}, this.timeout);
					};
				};
			},
		});
		
		loader = tools.createEventTarget(loader);
		
		return loader;
	})();
	
	tools.getJsScriptFileLoader = function getJsScriptFileLoader(url, /*optional*/async, /*optional*/timeout, /*optional*/reload) {
		doodad.ASSERT && doodad.ASSERT(tools.isStringAndNotEmpty(url), "Invalid url.");

		var loader = null;
		
		if (tools.hasKey(__loadedScripts__, url)) {
			loader = __loadedScripts__[url];
		};
		if (reload) {
			if (loader.element && loader.element.parentNode) {
				loader.element.parentNode.removeChild(loader.element);
				loader.element = null;
			};
			loader = null;
		};
		if (!loader) {
			__loadedScripts__[url] = loader = new tools.ScriptLoader(/*tag*/'script', /*target*/document.body, /*timeout*/timeout);
			
			loader.addEventListener('init', function() {
				loader.async = this.element.async = !!async;
				this.element.type = 'text/javascript';
				this.element.src = url;
			});
		};
		
		return loader;
	};
	
	tools.getJsScriptBlockLoader = function getJsScriptBlockLoader(script, /*optional*/async, /*optional*/timeout) {
		doodad.ASSERT && doodad.ASSERT(tools.isStringAndNotEmpty(script), "Invalid script.");

		var loader = new tools.ScriptLoader(/*tag*/'script', /*target*/document.body, /*timeout*/timeout);
		
		loader.addEventListener('init', function() {
			this.element.type = 'text/javascript';
			loader.async = this.element.async = !!async;
			if (async && window.Blob && window.URL) {
				// Firefox
				this.element.src = URL.createObjectURL(new Blob(script));
			} else {
				if (document.createCDATASection) {
					this.element.appendChild(document.createCDATASection(script));
				} else {
					this.element.innerHTML = script;
				};
			};
		});
		
		return loader;
	};
	
	tools.getCssScriptFileLoader = function getCssScriptFileLoader(url, /*optional*/async, /*optional*/media, /*optional*/timeout, /*optional*/reload) {
		doodad.ASSERT && doodad.ASSERT(tools.isStringAndNotEmpty(url), "Invalid url.");

		var loader = null;
		
		if (tools.hasKey(__loadedScripts__, url)) {
			loader = __loadedScripts__[url];
		};
		if (reload) {
			if (loader.element && loader.element.parentNode) {
				loader.element.parentNode.removeChild(loader.element);
				loader.element = null;
			};
			loader = null;
		};
		if (!loader) {
			__loadedScripts__[url] = loader = new tools.ScriptLoader(/*tag*/'link', /*target*/document.getElementsByTagName('head')[0], /*timeout*/timeout);
			
			loader.addEventListener('init', function() {
				loader.async = !!async;
				this.element.rel = 'stylesheet';
				this.element.type = 'text/css';
				if (media) {
					this.element.media = media;
				};
				this.element.href = url;
			});
		};
		
		return loader;
	};

	tools.getCssScriptBlockLoader = function getCssScriptBlockLoader(script, /*optional*/async, /*optional*/media, /*optional*/timeout) {
		doodad.ASSERT && doodad.ASSERT(tools.isStringAndNotEmpty(script), "Invalid script.");

		var loader;
		
		if (async && window.Blob && window.URL) {
			// Firefox
			loader = new tools.ScriptLoader(/*tag*/'link', /*target*/document.getElementsByTagName('head')[0], /*timeout*/timeout);

			loader.addEventListener('init', function() {
				loader.async = !!async;
				this.element.rel = 'stylesheet';
				this.element.type = 'text/css';
				if (media) {
					this.element.media = media;
				};
				this.element.href = URL.createObjectURL(new Blob(script));
			});
		} else {
			loader = new tools.ScriptLoader(/*tag*/'style', /*target*/document.getElementsByTagName('head')[0], /*timeout*/timeout);

			loader.addEventListener('init', function() {
				loader.async = !!async;
				if (media) {
					this.element.media = media;
				};
				if (document.createCDATASection) {
					this.element.appendChild(document.createCDATASection(script));
				} else {
					this.element.innerHTML = script;
				};
			});
		};
		
		return loader;
	};

	
	tools.safeEval = function safeEval(expression, /*optional*/locals, /*optional*/globals, /*optional*/allowFunctionCalls, /*optional*/allowNew) {
		// WARNING: You must be care with (allowFunctionCalls=true)
		// NOTE: This function has been written to safely evaluate simple JavaScript expressions (not the whole JavaScript syntax) to a single value.
		// NOTE: Caller functions should use "safeEvalCached" for performance issues (only when expressions are controlled and limited)
		// TODO: Implement forgotten and new validations
		
		// Takes care of end of statements (for simplicity, newlines are always an implicit end of statement)
		// Prevents multiple statements
		// Prevents function calls if not optionally allowed (allowFunctionCalls=true)
		// Prevents 'new' if not optionally allowed (allowNew=true)
		// Prevents assignments
		// Prevents increments
		// Restrict access to locals (locals={...}) and globals (globals=[...]).
		// Prevents access to my local variables and arguments
		// Globals can be accessed from "window".
		
		
		doodad.ASSERT && doodad.ASSERT(tools.isString(expression), "Invalid expression.");

		var evalFn = null,
			localsKeys = null;
			
		if (tools.isFunction(locals)) {
			evalFn = locals;
			locals = null;
			localsKeys = evalFn.localsKeys;
		};
		
		if (tools.isNothing(locals)) {
			locals = null;
		} else {
			doodad.ASSERT && doodad.ASSERT(tools.isObject(locals), "Invalid locals object.");
			localsKeys = tools.keys(locals);
		};

		if (tools.isNothing(globals)) {
			globals = null;
		} else {
			doodad.ASSERT && doodad.ASSERT(tools.isArray(globals), "Invalid globals array.");
		};
		
		var id = {  // use an object to allow changes by 'validateIdentifier'
				name: '',
				prevName: '',
				isTop: true,
				isWindow: false,
				isNew: false,
			},
			prevChr = '',
			isString = false,
			isEscape = false,
			stringChar = null,
			isTemplate = false,
			isTemplateSubstitution = false,
			isAssignment = false,
			isComment = false,
			isCommentBlock = false,
			hasStatement = false,
			chars = expression.split('');
			
		function validateIdentifier() {
			if (id.name.length) {
				if (id.isTop) {
					if (id.name === 'new') {
						if (!allowNew) {
							throw new Error("Access to 'new' is denied.");
						};
						id.isNew = true;
					} else if (tools.safeEval.allDigitsRegEx.test(id.name)) {
						id.name = '';  // no id.prevName
					} else if (tools.indexOf(tools.safeEval.constants, id.name) >= 0) {
						id.name = '';  // no id.prevName
					} else if (localsKeys && (tools.indexOf(localsKeys, id.name) >= 0)) {
					} else if (id.name === 'window') {
						id.isWindow = true;
					} else if (globals && (tools.indexOf(globals, id.name) >= 0)) {
					} else {
						throw new Error(tools.format("Access to '~0~' is denied.", [id.name]));
					};
				} else if (id.isWindow) {
					if (id.name !== 'window') {
						if (!globals || (tools.indexOf(globals, id.name) < 0)) {
							throw new Error(tools.format("Access to '~0~' is denied.", [id.name]));
						};
						id.isWindow = false;
					};
				};
				id.prevName = id.name;
				id.name = '';
				id.isTop = true;
			};
		};
			
		chars.push('\n');  // terminate statement so validateIdentifier is called
		for (var i = 0; i < chars.length; i++) {
			var chr = chars[i];
			
			if (isString) {
				if (isEscape) {
					// Escaped char
					isEscape = false;
				} else if (chr === '\\') {
					// String escape
					isEscape = true;
				} else if (chr === stringChar) {
					// String closure
					isString = false;
				};
			} else if (isTemplate && !isTemplateSubstitution) {
				if ((prevChr === '$') && (chr === '{')) {
					isTemplateSubstitution = true;
				} else if (isEscape) {
					// Escaped char
					isEscape = false;
				} else if (chr === '\\') {
					// Template escape
					isEscape = true;
				} else if (chr === '`') {
					// Template closure
					isTemplate = false;
				};
			} else if (isCommentBlock) {
				// Comment block
				if ((prevChr === '*') && (chr === '/')) {
					// End comment block
					isCommentBlock = false;
				};
			} else if (isAssignment && (chr !== '=')) {
				// Assignment
				throw new Error("Assignment is not allowed.");
			} else if (hasStatement) {
				// Begin a new statement after another one
				throw new Error("Multiple statements are not allowed.");
			} else if ((chr === ';') || (chr === '\n') || (chr === '\r')) {
				if (isComment && (chr === ';')) {
				} else {
					// New statement
					if (isTemplateSubstitution) {
						throw new Error("End of statement in template substitution is not allowed.");
					};
					validateIdentifier();
					hasStatement = true;
					isComment = false;
				};
			} else if (isComment) {
				// Statement comment
			} else if (tools.safeEval.alphaRegEx.test(chr)) {
				// Identifier
				id.name += chr;
			} else {
				validateIdentifier();
				
				if (tools.safeEval.spaceRegEx.test(chr)) {
					// Space
				} else if ((chr === '(') && id.prevName.length && !id.isNew && !allowFunctionCalls) {
					throw new Error("Function calls are not allowed.");
				} else if ((prevChr === '/') && (chr === '/')) {
					// Begin statement comment
					isComment = true;
				} else if ((prevChr === '/') && (chr === '*')) {
					// Begin comment block
					isCommentBlock = true;
				} else {
					// Operational chars
					isAssignment = false;
					id.isNew = false;
					
					if (isTemplateSubstitution && (chr === '{')) {
						// For simplicity
						throw new Error("'{' is denied in templates.");
					} else if (isTemplateSubstitution && (chr === '}')) {
						if (id.isWindow) {
							throw new Error("'window' can't be returned.");
						};
						isTemplateSubstitution = false;
						id.prevName = '';
					} else if ((chr === '"') || (chr === "'")) {
						// Begin String
						isString = true;
						stringChar = chr;
						id.prevName = '';
					} else if (chr === '`') {
						// Begin Template String
						if (isTemplateSubstitution) {
							// For simplicity
							throw new Error("Template string in template string is not allowed.");
						};
						if (id.prevName.length && !allowFunctionCalls) {
							// Template tag
							throw new Error("Function calls are not allowed.");
						};
						isTemplate = true;
						id.prevName = '';
					} else if (chr === '.') {
						// Property
						id.isTop = false;
					} else if ((chr === '+') || (chr === '-')) {
						if (prevChr === chr) {
							// Increment
							throw new Error("Increment operators are not allowed.");
						};
						// Simple addition
					} else if ((chr === '=') && ((prevChr !== '=') && (prevChr !== '!'))) {
						// Potential assignment
						isAssignment = true
					};
				};
			};
			
			prevChr = chr;
		};
		
		if (id.isWindow) {
			throw new Error("'window' can't be returned.");
		};

		if (!evalFn) {
			if (locals) {
				evalFn = tools.safeEval.createEval(localsKeys).apply(null, tools.values(locals));
			} else {
				evalFn = tools.safeEval.evalNoLocals;
			};
		};
		
		return evalFn(expression);
	};
	tools.safeEvalCached = function safeEvalCached(evalCacheObject, expression, /*optional*/locals, /*optional*/globals, /*optional*/allowFunctionCalls, /*optional*/allowNew) {
		// WARNING: If expressions are not controlled and limited, don't use this function because of memory overhead
		// WARNING: Always use same options for the same cache object
		
		doodad.ASSERT && doodad.ASSERT(tools.isObject(evalCacheObject), "Invalid cache object.");
		doodad.ASSERT && doodad.ASSERT(tools.isString(expression), "Invalid expression.");
		
		expression = expression.trim();

		var evalFn = evalCacheObject.__SAFE_EVAL__;
		
		if (!evalFn) {
			var localsFn = null,
				localsKeys = null;
			if (locals) {
				localsKeys = tools.keys(locals);
				localsFn = tools.safeEval.createEval(localsKeys);
			};
			if (localsFn) {
				evalFn = localsFn.apply(null, tools.values(locals));
			} else {
				evalFn = tools.safeEval.evalNoLocals;
			};
			evalFn.localsKeys = localsKeys;
			evalCacheObject.__SAFE_EVAL__ = evalFn;
		};
		
		if (expression === '__SAFE_EVAL__') {
			return tools.safeEval(expression, evalFn, globals, allowFunctionCalls, allowNew);
		} else if (tools.hasKey(evalCacheObject, expression)) {
			return evalCacheObject[expression];
		} else {
			return evalCacheObject[expression] = tools.safeEval(expression, evalFn, globals, allowFunctionCalls, allowNew);
		};
	};
	
	doodad.Tools = tools = new (tools.createEventTarget(tools, 'Doodad.Tools'))();

	Doodad.Tools.initSafeEval.call(tools);
	
	tools.initialized = true;
};

Doodad.Tools.initSafeEval = function() {
	// WARNING: Do not declare any variable and any parameter inside this function.

	this.extend(this.safeEval, {
		constants: ['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'],
		allDigitsRegEx: /^((\d)+|0x([0-9a-f])+|0b([01])+|0o([0-7])+)$/i,
		alphaRegEx: /(\w|[$])/,  // use regex to take care of all Unicode alphanumeric chars
		spaceRegEx: /\s/,  // use regex to take care of all Unicode space chars
		createEval: function(/*locals*/) {
			return eval(
				"(function(" + Array.prototype.join.call(arguments[0], ',') + ") {" +
					"return function(/*expression*/) {" +
						"return eval(arguments[0]);" +
					"};" +
				"})"
			);
		},
		evalNoLocals: function(/*expression*/) {
			return eval(arguments[0]);
		},
	});
};


// Doodad.Tools.init()
// a=1

// console.log("ALLOWED");
// console.log("=======");
// Doodad.Tools.safeEval("1")
// Doodad.Tools.safeEval("0.1")
// Doodad.Tools.safeEval(".1")
// Doodad.Tools.safeEval("+1")
// Doodad.Tools.safeEval("-1")
// Doodad.Tools.safeEval("+-1")
// Doodad.Tools.safeEval("-+1")
// Doodad.Tools.safeEval("'hello'")
// Doodad.Tools.safeEval("'hello \\'sir\\''")
// Doodad.Tools.safeEval("'a=1'")
// Doodad.Tools.safeEval("'a=1,\\'b=2\\''")
// Doodad.Tools.safeEval("a", null, ['a'])
// Doodad.Tools.safeEval("(a)", null, ['a'])
// Doodad.Tools.safeEval("window.a", null, ['a'])
// Doodad.Tools.safeEval("a==1", null, ['a'])
// Doodad.Tools.safeEval("a == 1", null, ['a'])
// Doodad.Tools.safeEval("a== 1", null, ['a'])
// Doodad.Tools.safeEval("a ==1", null, ['a'])
// Doodad.Tools.safeEval("a  ==1", null, ['a'])
// Doodad.Tools.safeEval("a  ==  1", null, ['a'])
// Doodad.Tools.safeEval("a!=1", null, ['a'])
// Doodad.Tools.safeEval("a===1", null, ['a'])
// Doodad.Tools.safeEval("a!==1", null, ['a'])
// Doodad.Tools.safeEval("a+1", null, ['a'])
// Doodad.Tools.safeEval("1+1")
// Doodad.Tools.safeEval("1 + 1")
// Doodad.Tools.safeEval("1 +1")
// Doodad.Tools.safeEval("1+ 1")
// Doodad.Tools.safeEval("1+(1)")
// Doodad.Tools.safeEval("(1+1)")
// Doodad.Tools.safeEval("(1+1)==2")
// Doodad.Tools.safeEval("(1+1)===2")
// Doodad.Tools.safeEval("true")  // true is a constant, not a globsl
// Doodad.Tools.safeEval("!true")
// Doodad.Tools.safeEval("true && !false")
// Doodad.Tools.safeEval("'hello;'")
// Doodad.Tools.safeEval("'var'")
// Doodad.Tools.safeEval("new Date", null, ['Date'], false, true)
// Doodad.Tools.safeEval("new Date()", null, ['Date'], false, true)
// Doodad.Tools.safeEval("value", {value: 1})
// Doodad.Tools.safeEval("0x10")
// Doodad.Tools.safeEval("0x10+1")
// Doodad.Tools.safeEval("0x10 + 1")
// Doodad.Tools.safeEval("0b10")  // Firefox Nightly
// Doodad.Tools.safeEval("0o10")  // Firefox Nightly
// Doodad.Tools.safeEval("`Hi ${a} !`", null, ['a'])  // Firefox Nightly
// Doodad.Tools.safeEval("`Hi ${who} !`", {who: "you"})  // Firefox Nightly
// Doodad.Tools.safeEval("`Hi ${who+', the geek'} !`", {who: "you"})  // Firefox Nightly
// Doodad.Tools.safeEval("tag`Hi ${who} !`", {tag: function() {return 'Hi !'}, who: "you"}, null, true)  // Firefox Nightly
// Doodad.Tools.safeEval("tag `Hi ${who} !`", {tag: function() {return 'Hi !'}, who: "you"}, null, true)  // Firefox Nightly
// Doodad.Tools.safeEval("`${new Date}`", null, ['Date'], false, true)  // Firefox Nightly
// Doodad.Tools.safeEval("`${new Date()}`", null, ['Date'], false, true)  // Firefox Nightly
// Doodad.Tools.safeEval("/*eval*/a", null, ['a'])
// Doodad.Tools.safeEval("/*a=1*/a", null, ['a'])
// Doodad.Tools.safeEval("a/*a=1*/", null, ['a'])
// Doodad.Tools.safeEval("a//a=1", null, ['a'])


// console.log("NOT ALLOWED");
// console.log("===========");
// Doodad.Tools.safeEval("a=1", null, ['a'])   // assignment
// Doodad.Tools.safeEval("a='hello'", null, ['a'])   // assignment
// Doodad.Tools.safeEval("a=`hello`", null, ['a'])   // Firefox Nightly: assignment
// Doodad.Tools.safeEval("a=+1", null, ['a'])   // assignment
// Doodad.Tools.safeEval("a=-1", null, ['a'])   // assignment
// Doodad.Tools.safeEval("a=b", null, ['a','b'])   // assignment
// Doodad.Tools.safeEval("a=;", null, ['a'])   // assignment
// Doodad.Tools.safeEval("(a)=1", null, ['a']) // assignment
// Doodad.Tools.safeEval("1=2")   // assignment
// Doodad.Tools.safeEval("a = 1", null, ['a']) // assignment
// Doodad.Tools.safeEval("a= 1", null, ['a'])  // assignment
// Doodad.Tools.safeEval("a =1", null, ['a'])  // assignment
// Doodad.Tools.safeEval("a=  1", null, ['a']) // assignment
// Doodad.Tools.safeEval("a  =1", null, ['a']) // assignment
// Doodad.Tools.safeEval("a+=1", null, ['a'])  // assignment
// Doodad.Tools.safeEval("a+='hello'", null, ['a'])   // assignment
// Doodad.Tools.safeEval("a+=`hello`", null, ['a'])   // Firefox Nightly: assignment
// Doodad.Tools.safeEval("'hello' a=3", null, ['a'])  // assignment
// Doodad.Tools.safeEval("a++", null, ['a'])   // incrementation
// Doodad.Tools.safeEval("a--", null, ['a'])   // incrementation
// Doodad.Tools.safeEval("a ++", null, ['a'])   // incrementation
// Doodad.Tools.safeEval("++a", null, ['a'])   // incrementation
// Doodad.Tools.safeEval("--a", null, ['a'])   // incrementation
// Doodad.Tools.safeEval("++ a", null, ['a'])   // incrementation
// Doodad.Tools.safeEval("alert('hello')", null, ['alert'])  // function call
// Doodad.Tools.safeEval("alert ('hello')", null, ['alert'])  // function call
// Doodad.Tools.safeEval("var a")  // var is denied
// Doodad.Tools.safeEval("expression")  // my local variables
// Doodad.Tools.safeEval("value()", {value: 1}) // function call
// Doodad.Tools.safeEval("1+1;1")   // multiple statements
// Doodad.Tools.safeEval("1\r\n2")  // multiple statements
// Doodad.Tools.safeEval("//comment\na", null, ['a']) // multiple statements
// Doodad.Tools.safeEval("eval")   // eval is denied
// Doodad.Tools.safeEval("eval('1')", null, null, true)   // eval is denied
// Doodad.Tools.safeEval("this") // this is denied
// Doodad.Tools.safeEval("window") // window is denied
// Doodad.Tools.safeEval("window.window") // window is denied
// Doodad.Tools.safeEval("window.eval") // eval is denied
// Doodad.Tools.safeEval("window.window.eval('1')", null, null, true) // eval is denied
// Doodad.Tools.safeEval("a") // 'a' is denied
// Doodad.Tools.safeEval("window.a") // 'a' is denied
// Doodad.Tools.safeEval("document") // 'document' is denied
// Doodad.Tools.safeEval("window.document") // 'document' is denied
// Doodad.Tools.safeEval("window.document.body") // 'document' is denied
// Doodad.Tools.safeEval("(function() {return 1})()")   // function denied
// Doodad.Tools.safeEval("`Hi ${a} !`")  // Firefox Nightly: a denied
// Doodad.Tools.safeEval("`Hi ${this} !`")  // Firefox Nightly: this is denied
// Doodad.Tools.safeEval("`Hi ${who='me'} !`", {who: "you"})  // Firefox Nightly: assignment
// Doodad.Tools.safeEval("`Hi ${who+=' too'} !`", {who: "you"})  // Firefox Nightly: assignment
// Doodad.Tools.safeEval("`Hi ${who++} !`", {who: 0})  // Firefox Nightly:  increment
// Doodad.Tools.safeEval("`Hi ${who;} !`", {who: "you"})  // Firefox Nightly: statement in template
// Doodad.Tools.safeEval("`Hi ${`${who}`} !`", {who: "you"})  // Firefox Nightly: template in template denied
// Doodad.Tools.safeEval("`${new Date}`", null, ['Date'])  // Firefox Nightly:  new denied
// Doodad.Tools.safeEval("`${(function(){return '1'})()}`")  // Firefox Nightly: function denied
// Doodad.Tools.safeEval("a`hello`", null, ['a'])  // Firefox Nightly: function call
// Doodad.Tools.safeEval("a`hello`")  // Firefox Nightly: a is denied
// Doodad.Tools.safeEval("tag`hello`", {tag: function() {return 'bye'}})  // Firefox Nightly: function call
// Doodad.Tools.safeEval("eval`alert('hello'}`")  // Firefox Nightly: eval is denied
// Doodad.Tools.safeEval("window.eval`alert('hello'}`")  // Firefox Nightly: eval is denied
// Doodad.Tools.safeEval("/*comment*/eval")  // eval is denied
// Doodad.Tools.safeEval("eval/*comment*/")  // eval is denied
// Doodad.Tools.safeEval("eval//comment")  // eval is denied
